Text Classification
Transformers
Safetensors
code
roberta
clone-detection
graphcodebert
code-similarity
Eval Results (legacy)
text-embeddings-inference
Instructions to use thealper2/graphcodebert-code-clone-detection with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use thealper2/graphcodebert-code-clone-detection with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-classification", model="thealper2/graphcodebert-code-clone-detection")# Load model directly from transformers import AutoTokenizer, GraphCodeBERTForCloneDetection tokenizer = AutoTokenizer.from_pretrained("thealper2/graphcodebert-code-clone-detection") model = GraphCodeBERTForCloneDetection.from_pretrained("thealper2/graphcodebert-code-clone-detection", device_map="auto") - Notebooks
- Google Colab
- Kaggle
Add GraphCodeBERT clone-detection model
Browse files- README.md +184 -0
- code/config.py +275 -0
- code/dfg_parser.py +421 -0
- code/modeling.py +193 -0
- code/preprocess.py +839 -0
- code/requirements.txt +17 -0
- config.json +30 -0
- experiment_record.json +298 -0
- model.safetensors +3 -0
- tokenizer.json +0 -0
- tokenizer_config.json +17 -0
- training_args.bin +3 -0
- training_config.json +51 -0
README.md
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: mit
|
| 3 |
+
library_name: transformers
|
| 4 |
+
pipeline_tag: text-classification
|
| 5 |
+
tags:
|
| 6 |
+
- code
|
| 7 |
+
- clone-detection
|
| 8 |
+
- graphcodebert
|
| 9 |
+
- code-similarity
|
| 10 |
+
base_model: microsoft/graphcodebert-base
|
| 11 |
+
datasets:
|
| 12 |
+
- PoolC/1-fold-clone-detection-600k-5fold
|
| 13 |
+
language:
|
| 14 |
+
- code
|
| 15 |
+
metrics:
|
| 16 |
+
- accuracy
|
| 17 |
+
- precision
|
| 18 |
+
- recall
|
| 19 |
+
- f1
|
| 20 |
+
model-index:
|
| 21 |
+
- name: graphcodebert-code-clone-detection
|
| 22 |
+
results:
|
| 23 |
+
- task:
|
| 24 |
+
type: text-classification
|
| 25 |
+
name: Binary code clone detection
|
| 26 |
+
dataset:
|
| 27 |
+
type: PoolC/1-fold-clone-detection-600k-5fold
|
| 28 |
+
name: PoolC/1-fold-clone-detection-600k-5fold
|
| 29 |
+
split: test (group-disjoint half of the `val` fold)
|
| 30 |
+
metrics:
|
| 31 |
+
- type: f1
|
| 32 |
+
value: 0.8805
|
| 33 |
+
- type: accuracy
|
| 34 |
+
value: 0.8747
|
| 35 |
+
- type: precision
|
| 36 |
+
value: 0.841
|
| 37 |
+
- type: recall
|
| 38 |
+
value: 0.924
|
| 39 |
+
---
|
| 40 |
+
|
| 41 |
+
# graphcodebert-code-clone-detection
|
| 42 |
+
|
| 43 |
+
Binary code-clone detection. Full fine-tune of
|
| 44 |
+
[`microsoft/graphcodebert-base`](https://huggingface.co/microsoft/graphcodebert-base) on
|
| 45 |
+
[`PoolC/1-fold-clone-detection-600k-5fold`](https://huggingface.co/datasets/PoolC/1-fold-clone-detection-600k-5fold),
|
| 46 |
+
using GraphCodeBERT's data-flow-aware pairwise architecture.
|
| 47 |
+
|
| 48 |
+
Output labels: `0 = not clone`, `1 = clone`.
|
| 49 |
+
|
| 50 |
+
## Architecture
|
| 51 |
+
|
| 52 |
+
Not a generic sequence-pair classifier. The two snippets are encoded
|
| 53 |
+
**separately** by one shared GraphCodeBERT encoder, each with its own
|
| 54 |
+
graph-guided masked attention, and the two `<s>` vectors are concatenated for
|
| 55 |
+
classification:
|
| 56 |
+
|
| 57 |
+
```
|
| 58 |
+
Linear(2 x 768 -> 768) -> tanh -> Linear(768 -> 2)
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
Per-snippet input layout (length 640):
|
| 62 |
+
|
| 63 |
+
| segment | length | content | `position_idx` |
|
| 64 |
+
|---|---|---|---|
|
| 65 |
+
| code tokens | 512 | `<s>` + BPE code tokens + `</s>` | `2 .. n+1` |
|
| 66 |
+
| data-flow nodes | 128 | one slot per DFG variable node (`<unk>` id) | `0` |
|
| 67 |
+
| padding | remainder | `<pad>` | `1` |
|
| 68 |
+
|
| 69 |
+
A data-flow node's input embedding is the **average of the embeddings of the
|
| 70 |
+
code tokens it was identified from**. Graph-guided attention allows: code to
|
| 71 |
+
code; `<s>`/`</s>` to everything; node to the code tokens it comes from (and
|
| 72 |
+
back); node to adjacent nodes.
|
| 73 |
+
|
| 74 |
+
## Preprocessing
|
| 75 |
+
|
| 76 |
+
The dataset contains **Python** snippets, so data flow is extracted with the
|
| 77 |
+
`tree-sitter-python` grammar via a port of GraphCodeBERT's `DFG_python`
|
| 78 |
+
extractor (comment/docstring stripping -> AST -> variable states ->
|
| 79 |
+
`comesFrom` / `computedFrom` edges).
|
| 80 |
+
|
| 81 |
+
| | |
|
| 82 |
+
|---|---|
|
| 83 |
+
| `code_length` | 512 |
|
| 84 |
+
| `data_flow_length` | 128 |
|
| 85 |
+
| total sequence length | 640 |
|
| 86 |
+
| distinct snippets featurised | 44,950 |
|
| 87 |
+
| mean data-flow nodes / snippet | 44.22 |
|
| 88 |
+
| snippets with empty data flow | 263 |
|
| 89 |
+
| total data-flow edges | 2,481,388 |
|
| 90 |
+
| extraction status counts | `{"ok": 44930, "comment_strip_failed": 13, "dfg_failed": 7}` |
|
| 91 |
+
|
| 92 |
+
No example was dropped: a snippet whose data flow could not be extracted is
|
| 93 |
+
kept with an empty graph and counted above.
|
| 94 |
+
|
| 95 |
+
## Data splits
|
| 96 |
+
|
| 97 |
+
The repository provides one of 5 predefined folds as `train` + `val`; those groups are disjoint and are kept as-is. `val` is partitioned further into validation/test along problem-group boundaries.
|
| 98 |
+
|
| 99 |
+
`similar` equals `(code1_group == code2_group)` for every row, so the group
|
| 100 |
+
columns are a perfect label proxy and are never used as features.
|
| 101 |
+
|
| 102 |
+
| split | source | pairs | positives | negatives | groups |
|
| 103 |
+
|---|---|---:|---:|---:|---:|
|
| 104 |
+
| train | `train` fold | 50,000 | 25,000 | 25,000 | 240 |
|
| 105 |
+
| validation | half of `val` by group | 20,000 | 10,000 | 10,000 | 29 |
|
| 106 |
+
| test | other half of `val` by group | 20,000 | 10,000 | 10,000 | 30 |
|
| 107 |
+
|
| 108 |
+
Train/validation/test share **no problem group and no code snippet**; this is
|
| 109 |
+
asserted at runtime before training starts. 337,398 pairs of the
|
| 110 |
+
held-out fold were dropped because their two snippets fell on opposite sides of
|
| 111 |
+
the validation/test group boundary.
|
| 112 |
+
|
| 113 |
+
Class weighting: Measured majority-class share 0.5000 is within the 0.6 threshold, so weighted cross entropy is NOT used.
|
| 114 |
+
|
| 115 |
+
## Training
|
| 116 |
+
|
| 117 |
+
| | |
|
| 118 |
+
|---|---|
|
| 119 |
+
| optimizer | adamw_torch |
|
| 120 |
+
| learning rate | 2e-05 |
|
| 121 |
+
| scheduler | linear with 0.1 warmup ratio (938 steps) |
|
| 122 |
+
| epochs | 3.0 |
|
| 123 |
+
| per-device batch size | 16 |
|
| 124 |
+
| gradient accumulation | 1 |
|
| 125 |
+
| effective batch size | 16 |
|
| 126 |
+
| weight decay | 0.01 |
|
| 127 |
+
| gradient clipping | 1.0 |
|
| 128 |
+
| mixed precision | fp16 |
|
| 129 |
+
| gradient checkpointing | False |
|
| 130 |
+
| seed | 42 |
|
| 131 |
+
| trainable parameters | 125,236,994 |
|
| 132 |
+
| training time | 1.923 h |
|
| 133 |
+
| GPU | NVIDIA GeForce RTX 5060 Ti (15.9 GB) |
|
| 134 |
+
| torch / transformers | 2.11.0+cu128 / 5.17.0 |
|
| 135 |
+
|
| 136 |
+
Checkpoint selection: best validation **F1** (`load_best_model_at_end=True`,
|
| 137 |
+
`metric_for_best_model="f1"`). Best validation F1 = **0.8672**.
|
| 138 |
+
The test split was scored once, after selection.
|
| 139 |
+
|
| 140 |
+
## Results
|
| 141 |
+
|
| 142 |
+
| split | accuracy | precision | recall | F1 | TP | TN | FP | FN |
|
| 143 |
+
|---|---:|---:|---:|---:|---:|---:|---:|---:|
|
| 144 |
+
| validation | 0.8557 | 0.8032 | 0.9422 | 0.8672 | 9,422 | 7,692 | 2,308 | 578 |
|
| 145 |
+
| test | 0.8747 | 0.8410 | 0.9240 | 0.8805 | 9,240 | 8,253 | 1,747 | 760 |
|
| 146 |
+
|
| 147 |
+
Test confusion matrix (`[[TN, FP], [FN, TP]]`): `[[8253, 1747], [760, 9240]]`
|
| 148 |
+
|
| 149 |
+
## Usage
|
| 150 |
+
|
| 151 |
+
This checkpoint uses a **custom pairwise head and a graph-guided attention
|
| 152 |
+
mask**, so `AutoModelForSequenceClassification` will not reproduce these
|
| 153 |
+
results. Use the repository's own model class and preprocessing:
|
| 154 |
+
|
| 155 |
+
```python
|
| 156 |
+
import torch
|
| 157 |
+
from transformers import AutoTokenizer
|
| 158 |
+
from modeling import load_model # from this project
|
| 159 |
+
from preprocess import build_snippet_features, CloneCollator
|
| 160 |
+
from config import Config
|
| 161 |
+
|
| 162 |
+
cfg = Config()
|
| 163 |
+
tokenizer = AutoTokenizer.from_pretrained("thealper2/graphcodebert-code-clone-detection")
|
| 164 |
+
model = load_model("thealper2/graphcodebert-code-clone-detection").eval()
|
| 165 |
+
|
| 166 |
+
features = build_snippet_features(cfg, [code_a, code_b], tokenizer, num_proc=1)
|
| 167 |
+
collator = CloneCollator(features)
|
| 168 |
+
batch = collator([(0, 1, 0)]) # (snippet_a, snippet_b, dummy label)
|
| 169 |
+
with torch.no_grad():
|
| 170 |
+
logits = model(**{k: v for k, v in batch.items() if k != "labels"}).logits
|
| 171 |
+
label = int(logits.argmax(-1)) # 0 = not clone, 1 = clone
|
| 172 |
+
```
|
| 173 |
+
|
| 174 |
+
## Limitations
|
| 175 |
+
|
| 176 |
+
- Trained on competitive-programming Python solutions grouped by problem;
|
| 177 |
+
"clone" therefore means *solves the same problem*, which is closer to
|
| 178 |
+
semantic (Type-4) similarity than to syntactic copy-paste detection.
|
| 179 |
+
- Data flow is extracted with the Python grammar only. Other languages need the
|
| 180 |
+
matching `tree_sitter_<lang>` grammar and `DFG_<lang>` function.
|
| 181 |
+
- Snippets longer than 512 BPE tokens are truncated; 885 of
|
| 182 |
+
44,950 distinct snippets hit that limit.
|
| 183 |
+
- Both directions of a pair are not explicitly symmetrised; the head sees
|
| 184 |
+
`concat(<s>_1, <s>_2)` in the given order.
|
code/config.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Central configuration for GraphCodeBERT binary code-clone detection.
|
| 2 |
+
|
| 3 |
+
Every tunable lives in :class:`Config`. ``Config.from_cli`` turns the dataclass
|
| 4 |
+
fields into ``argparse`` flags automatically, so ``train.py`` / ``evaluate.py``
|
| 5 |
+
never drift apart from this file.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import argparse
|
| 11 |
+
import dataclasses
|
| 12 |
+
import json
|
| 13 |
+
from dataclasses import dataclass, field, fields
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
from typing import Any
|
| 16 |
+
|
| 17 |
+
# --------------------------------------------------------------------------- #
|
| 18 |
+
# Facts discovered by inspecting the dataset (see README "Dataset structure").
|
| 19 |
+
# They are kept here as documented defaults, *not* as blind assumptions:
|
| 20 |
+
# preprocess.py re-verifies every one of them at runtime and raises if the
|
| 21 |
+
# remote dataset ever changes.
|
| 22 |
+
# --------------------------------------------------------------------------- #
|
| 23 |
+
CODE1_COLUMN = "code1"
|
| 24 |
+
CODE2_COLUMN = "code2"
|
| 25 |
+
LABEL_COLUMN = "similar"
|
| 26 |
+
GROUP1_COLUMN = "code1_group"
|
| 27 |
+
GROUP2_COLUMN = "code2_group"
|
| 28 |
+
#: Metadata columns that MUST NOT reach the model. ``code1_group``/``code2_group``
|
| 29 |
+
#: determine the label exactly (``similar == (code1_group == code2_group)``), so
|
| 30 |
+
#: feeding them in any form would be a 100 % label leak.
|
| 31 |
+
FORBIDDEN_FEATURE_COLUMNS = (
|
| 32 |
+
GROUP1_COLUMN,
|
| 33 |
+
GROUP2_COLUMN,
|
| 34 |
+
"pair_id",
|
| 35 |
+
"question_pair_id",
|
| 36 |
+
)
|
| 37 |
+
#: The source language of the snippets, needed to pick the tree-sitter grammar.
|
| 38 |
+
DATASET_LANGUAGE = "python"
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
@dataclass
|
| 42 |
+
class Config:
|
| 43 |
+
"""All knobs for preprocessing, training and evaluation."""
|
| 44 |
+
|
| 45 |
+
# ---------------- dataset ---------------- #
|
| 46 |
+
dataset_name: str = "PoolC/1-fold-clone-detection-600k-5fold"
|
| 47 |
+
#: HF split that becomes the training set (group-disjoint from ``val``).
|
| 48 |
+
train_split: str = "train"
|
| 49 |
+
#: HF split that is partitioned by *group* into validation and test.
|
| 50 |
+
heldout_split: str = "val"
|
| 51 |
+
#: Fraction of the held-out split's groups reserved for the test set.
|
| 52 |
+
test_group_fraction: float = 0.5
|
| 53 |
+
|
| 54 |
+
#: Cap on the number of pairs per split. ``-1`` = use everything.
|
| 55 |
+
#: The full training split has 5.39 M pairs; see README for why the default
|
| 56 |
+
#: is a subsample and how to raise it.
|
| 57 |
+
max_train_samples: int = 50_000
|
| 58 |
+
max_eval_samples: int = 20_000
|
| 59 |
+
max_test_samples: int = 20_000
|
| 60 |
+
#: Keep the 50/50 label balance exactly when subsampling.
|
| 61 |
+
balance_subsamples: bool = True
|
| 62 |
+
|
| 63 |
+
# ---------------- model ---------------- #
|
| 64 |
+
model_name_or_path: str = "microsoft/graphcodebert-base"
|
| 65 |
+
#: GraphCodeBERT clone-detection defaults from the original paper/repo.
|
| 66 |
+
code_length: int = 512
|
| 67 |
+
data_flow_length: int = 128
|
| 68 |
+
attn_implementation: str = "sdpa"
|
| 69 |
+
|
| 70 |
+
# ---------------- training ---------------- #
|
| 71 |
+
learning_rate: float = 2e-5
|
| 72 |
+
num_train_epochs: float = 3.0
|
| 73 |
+
per_device_train_batch_size: int = 4
|
| 74 |
+
per_device_eval_batch_size: int = 4
|
| 75 |
+
gradient_accumulation_steps: int = 4
|
| 76 |
+
weight_decay: float = 0.01
|
| 77 |
+
warmup_ratio: float = 0.1
|
| 78 |
+
max_grad_norm: float = 1.0
|
| 79 |
+
fp16: bool = True
|
| 80 |
+
bf16: bool = False
|
| 81 |
+
gradient_checkpointing: bool = False
|
| 82 |
+
optim: str = "adamw_torch"
|
| 83 |
+
lr_scheduler_type: str = "linear"
|
| 84 |
+
|
| 85 |
+
#: Class-weighted cross entropy. ``"auto"`` enables it only when the
|
| 86 |
+
#: measured training distribution is more skewed than
|
| 87 |
+
#: ``class_weight_threshold``; ``"off"`` never, ``"on"`` always.
|
| 88 |
+
class_weighting: str = "auto"
|
| 89 |
+
class_weight_threshold: float = 0.6
|
| 90 |
+
|
| 91 |
+
# ---------------- evaluation / checkpointing ---------------- #
|
| 92 |
+
eval_strategy: str = "steps"
|
| 93 |
+
eval_steps: int = 1000
|
| 94 |
+
save_strategy: str = "steps"
|
| 95 |
+
save_steps: int = 1000
|
| 96 |
+
save_total_limit: int = 2
|
| 97 |
+
logging_steps: int = 100
|
| 98 |
+
metric_for_best_model: str = "f1"
|
| 99 |
+
greater_is_better: bool = True
|
| 100 |
+
load_best_model_at_end: bool = True
|
| 101 |
+
|
| 102 |
+
# ---------------- runtime ---------------- #
|
| 103 |
+
seed: int = 42
|
| 104 |
+
full_determinism: bool = False
|
| 105 |
+
dataloader_num_workers: int = 4
|
| 106 |
+
#: Processes used for tree-sitter data-flow extraction.
|
| 107 |
+
preprocessing_num_workers: int = 8
|
| 108 |
+
output_dir: str = "./outputs"
|
| 109 |
+
model_dir: str = "./models/graphcodebert-clone-detection"
|
| 110 |
+
logging_dir: str = "./logs"
|
| 111 |
+
cache_dir: str = "./outputs/feature_cache"
|
| 112 |
+
report_to: str = "none"
|
| 113 |
+
run_sanity_check: bool = True
|
| 114 |
+
sanity_check_samples: int = 64
|
| 115 |
+
|
| 116 |
+
# ---------------- derived ---------------- #
|
| 117 |
+
@property
|
| 118 |
+
def total_sequence_length(self) -> int:
|
| 119 |
+
"""Length of one encoded snippet: code tokens + data-flow nodes."""
|
| 120 |
+
return self.code_length + self.data_flow_length
|
| 121 |
+
|
| 122 |
+
@property
|
| 123 |
+
def effective_batch_size(self) -> int:
|
| 124 |
+
return self.per_device_train_batch_size * self.gradient_accumulation_steps
|
| 125 |
+
|
| 126 |
+
# ---------------- (de)serialisation ---------------- #
|
| 127 |
+
def to_dict(self) -> dict[str, Any]:
|
| 128 |
+
d = dataclasses.asdict(self)
|
| 129 |
+
d["total_sequence_length"] = self.total_sequence_length
|
| 130 |
+
d["effective_batch_size"] = self.effective_batch_size
|
| 131 |
+
return d
|
| 132 |
+
|
| 133 |
+
def save(self, path: str | Path) -> None:
|
| 134 |
+
path = Path(path)
|
| 135 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 136 |
+
path.write_text(json.dumps(self.to_dict(), indent=2), encoding="utf-8")
|
| 137 |
+
|
| 138 |
+
@classmethod
|
| 139 |
+
def from_json(cls, path: str | Path) -> "Config":
|
| 140 |
+
raw = json.loads(Path(path).read_text(encoding="utf-8"))
|
| 141 |
+
known = {f.name for f in fields(cls)}
|
| 142 |
+
return cls(**{k: v for k, v in raw.items() if k in known})
|
| 143 |
+
|
| 144 |
+
# ---------------- CLI ---------------- #
|
| 145 |
+
@classmethod
|
| 146 |
+
def build_parser(cls, description: str = "") -> argparse.ArgumentParser:
|
| 147 |
+
parser = argparse.ArgumentParser(
|
| 148 |
+
description=description,
|
| 149 |
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
| 150 |
+
)
|
| 151 |
+
parser.add_argument(
|
| 152 |
+
"--config_json",
|
| 153 |
+
type=str,
|
| 154 |
+
default=None,
|
| 155 |
+
help="Load defaults from a saved training_config.json, then apply CLI overrides.",
|
| 156 |
+
)
|
| 157 |
+
for f in fields(cls):
|
| 158 |
+
flag = f"--{f.name}"
|
| 159 |
+
if f.type is bool or f.type == "bool":
|
| 160 |
+
# Accept --flag / --flag true / --flag false.
|
| 161 |
+
parser.add_argument(
|
| 162 |
+
flag,
|
| 163 |
+
type=_str2bool,
|
| 164 |
+
nargs="?",
|
| 165 |
+
const=True,
|
| 166 |
+
default=None,
|
| 167 |
+
help=f"(bool) default: {f.default}",
|
| 168 |
+
)
|
| 169 |
+
else:
|
| 170 |
+
parser.add_argument(flag, type=type(f.default), default=None)
|
| 171 |
+
return parser
|
| 172 |
+
|
| 173 |
+
@classmethod
|
| 174 |
+
def from_cli(cls, argv: list[str] | None = None, description: str = "") -> "Config":
|
| 175 |
+
parser = cls.build_parser(description)
|
| 176 |
+
args, unknown = parser.parse_known_args(argv)
|
| 177 |
+
if unknown:
|
| 178 |
+
raise SystemExit(f"Unrecognised arguments: {unknown}")
|
| 179 |
+
cfg = cls.from_json(args.config_json) if args.config_json else cls()
|
| 180 |
+
for f in fields(cls):
|
| 181 |
+
value = getattr(args, f.name, None)
|
| 182 |
+
if value is not None:
|
| 183 |
+
setattr(cfg, f.name, value)
|
| 184 |
+
cfg.validate()
|
| 185 |
+
return cfg
|
| 186 |
+
|
| 187 |
+
def validate(self) -> None:
|
| 188 |
+
"""Fail fast on impossible combinations instead of dying mid-training."""
|
| 189 |
+
if self.fp16 and self.bf16:
|
| 190 |
+
raise ValueError("Enable at most one of fp16 / bf16.")
|
| 191 |
+
if not 0.0 < self.test_group_fraction < 1.0:
|
| 192 |
+
raise ValueError("test_group_fraction must lie strictly between 0 and 1.")
|
| 193 |
+
if self.code_length <= 3:
|
| 194 |
+
raise ValueError("code_length must leave room for <s> and </s>.")
|
| 195 |
+
if self.data_flow_length < 0:
|
| 196 |
+
raise ValueError("data_flow_length must be >= 0.")
|
| 197 |
+
# GraphCodeBERT position ids run up to code_length + 1; RoBERTa's
|
| 198 |
+
# embedding table holds 514 slots (512 + <pad> + offset).
|
| 199 |
+
if self.code_length > 512:
|
| 200 |
+
raise ValueError(
|
| 201 |
+
"code_length > 512 exceeds GraphCodeBERT's position embeddings (514 slots)."
|
| 202 |
+
)
|
| 203 |
+
if self.class_weighting not in {"auto", "on", "off"}:
|
| 204 |
+
raise ValueError("class_weighting must be one of: auto, on, off.")
|
| 205 |
+
if self.metric_for_best_model not in {
|
| 206 |
+
"f1",
|
| 207 |
+
"accuracy",
|
| 208 |
+
"precision",
|
| 209 |
+
"recall",
|
| 210 |
+
"loss",
|
| 211 |
+
}:
|
| 212 |
+
raise ValueError(f"Unsupported metric_for_best_model: {self.metric_for_best_model}")
|
| 213 |
+
if self.load_best_model_at_end and self.eval_strategy != self.save_strategy:
|
| 214 |
+
raise ValueError("load_best_model_at_end requires eval_strategy == save_strategy.")
|
| 215 |
+
if (
|
| 216 |
+
self.load_best_model_at_end
|
| 217 |
+
and self.eval_strategy == "steps"
|
| 218 |
+
and self.save_steps % self.eval_steps != 0
|
| 219 |
+
):
|
| 220 |
+
raise ValueError("save_steps must be a multiple of eval_steps.")
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
#: Placeholder namespace in the Makefile default; replaced by the logged-in user.
|
| 224 |
+
PLACEHOLDER_HUB_NAMESPACE = "your-username"
|
| 225 |
+
DEFAULT_HUB_MODEL_NAME = "graphcodebert-clone-detection"
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
def resolve_hub_repo_id(repo_id: str | None, token: str | None = None) -> str:
|
| 229 |
+
"""Expand a bare model name into ``<namespace>/<name>`` for the Hub.
|
| 230 |
+
|
| 231 |
+
Accepts ``None``, a bare name, or a full ``user/name``. The namespace is
|
| 232 |
+
taken from the caller's Hugging Face credential (``huggingface-cli login``,
|
| 233 |
+
``HF_TOKEN``, or an explicit ``token``), so the token never has to be typed
|
| 234 |
+
on the command line.
|
| 235 |
+
|
| 236 |
+
Raises:
|
| 237 |
+
RuntimeError: if no namespace is given and no credential is available.
|
| 238 |
+
"""
|
| 239 |
+
from huggingface_hub import HfApi, get_token
|
| 240 |
+
|
| 241 |
+
name = (repo_id or DEFAULT_HUB_MODEL_NAME).strip().strip("/")
|
| 242 |
+
if "/" in name:
|
| 243 |
+
namespace, _, model_name = name.partition("/")
|
| 244 |
+
if namespace != PLACEHOLDER_HUB_NAMESPACE:
|
| 245 |
+
return f"{namespace}/{model_name}"
|
| 246 |
+
name = model_name or DEFAULT_HUB_MODEL_NAME
|
| 247 |
+
|
| 248 |
+
effective = token or get_token()
|
| 249 |
+
if not effective:
|
| 250 |
+
raise RuntimeError(
|
| 251 |
+
"No Hugging Face credential found, so the namespace for "
|
| 252 |
+
f"{name!r} cannot be resolved. Run `huggingface-cli login`, export "
|
| 253 |
+
"HF_TOKEN, or pass the full repo id as HF_REPO=user/name."
|
| 254 |
+
)
|
| 255 |
+
try:
|
| 256 |
+
who = HfApi().whoami(token=effective)
|
| 257 |
+
except Exception as exc:
|
| 258 |
+
raise RuntimeError(
|
| 259 |
+
f"Could not identify the logged-in Hugging Face account: {exc}. "
|
| 260 |
+
"Pass the full repo id as HF_REPO=user/name."
|
| 261 |
+
) from exc
|
| 262 |
+
namespace = who.get("name")
|
| 263 |
+
if not namespace:
|
| 264 |
+
raise RuntimeError("Hugging Face account has no username; pass HF_REPO=user/name.")
|
| 265 |
+
return f"{namespace}/{name}"
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def _str2bool(value: str | bool) -> bool:
|
| 269 |
+
if isinstance(value, bool):
|
| 270 |
+
return value
|
| 271 |
+
if value.lower() in {"true", "t", "yes", "y", "1"}:
|
| 272 |
+
return True
|
| 273 |
+
if value.lower() in {"false", "f", "no", "n", "0"}:
|
| 274 |
+
return False
|
| 275 |
+
raise argparse.ArgumentTypeError(f"Expected a boolean, got {value!r}")
|
code/dfg_parser.py
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Data-flow graph (DFG) extraction for GraphCodeBERT.
|
| 2 |
+
|
| 3 |
+
This is a faithful port of Microsoft's GraphCodeBERT ``parser/`` package
|
| 4 |
+
(``utils.py`` + the ``DFG_python`` extractor from ``DFG.py``), adapted to the
|
| 5 |
+
modern ``py-tree-sitter`` API (>= 0.22, ``Language(tree_sitter_python.language())``)
|
| 6 |
+
instead of the original hand-compiled ``my-languages.so``.
|
| 7 |
+
|
| 8 |
+
The dataset used in this project contains **Python** snippets (verified in
|
| 9 |
+
``preprocess.py``), so only the Python extractor is ported; adding another
|
| 10 |
+
language means adding its ``DFG_<lang>`` function and grammar package here.
|
| 11 |
+
|
| 12 |
+
A DFG entry is the 5-tuple used throughout GraphCodeBERT::
|
| 13 |
+
|
| 14 |
+
(variable_name, token_index, edge_type, source_variable_names, source_token_indices)
|
| 15 |
+
|
| 16 |
+
``edge_type`` is ``"comesFrom"`` (value flows from a previous definition) or
|
| 17 |
+
``"computedFrom"`` (value is computed from the right-hand side of an assignment).
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
|
| 22 |
+
import io
|
| 23 |
+
import re
|
| 24 |
+
import sys
|
| 25 |
+
import tokenize
|
| 26 |
+
from typing import Any
|
| 27 |
+
|
| 28 |
+
from tree_sitter import Language, Node, Parser
|
| 29 |
+
|
| 30 |
+
__all__ = [
|
| 31 |
+
"get_parser",
|
| 32 |
+
"extract_dataflow",
|
| 33 |
+
"remove_comments_and_docstrings",
|
| 34 |
+
"DataFlowExtractionError",
|
| 35 |
+
]
|
| 36 |
+
|
| 37 |
+
#: tree-sitter recursion is mirrored by the recursive Python walkers below.
|
| 38 |
+
#: Competitive-programming snippets can nest deeply, so raise the ceiling but
|
| 39 |
+
#: keep it bounded so a pathological file raises RecursionError instead of
|
| 40 |
+
#: segfaulting the worker.
|
| 41 |
+
_RECURSION_LIMIT = 10_000
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class DataFlowExtractionError(RuntimeError):
|
| 45 |
+
"""Raised when a snippet cannot be turned into code tokens at all."""
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
_PARSER_CACHE: dict[str, Parser] = {}
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def get_parser(language: str = "python") -> Parser:
|
| 52 |
+
"""Return a cached tree-sitter parser for ``language``.
|
| 53 |
+
|
| 54 |
+
Cached per process so that ``datasets.map(num_proc=...)`` workers each build
|
| 55 |
+
the parser once rather than once per snippet.
|
| 56 |
+
"""
|
| 57 |
+
if language in _PARSER_CACHE:
|
| 58 |
+
return _PARSER_CACHE[language]
|
| 59 |
+
if language != "python":
|
| 60 |
+
raise ValueError(
|
| 61 |
+
f"Only the Python grammar is wired up (requested {language!r}). "
|
| 62 |
+
"Add the matching tree_sitter_<lang> package and DFG_<lang> function."
|
| 63 |
+
)
|
| 64 |
+
try:
|
| 65 |
+
import tree_sitter_python
|
| 66 |
+
except ImportError as exc: # pragma: no cover - environment problem
|
| 67 |
+
raise ImportError(
|
| 68 |
+
"tree_sitter_python is required for GraphCodeBERT data-flow extraction. "
|
| 69 |
+
"Install it with `pip install tree-sitter tree-sitter-python`."
|
| 70 |
+
) from exc
|
| 71 |
+
parser = Parser(Language(tree_sitter_python.language()))
|
| 72 |
+
_PARSER_CACHE[language] = parser
|
| 73 |
+
return parser
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
# --------------------------------------------------------------------------- #
|
| 77 |
+
# parser/utils.py
|
| 78 |
+
# --------------------------------------------------------------------------- #
|
| 79 |
+
def remove_comments_and_docstrings(source: str, lang: str = "python") -> str:
|
| 80 |
+
"""Strip comments and docstrings, preserving token columns.
|
| 81 |
+
|
| 82 |
+
Column positions are preserved because the DFG indices are ``(row, column)``
|
| 83 |
+
points into the *cleaned* source.
|
| 84 |
+
"""
|
| 85 |
+
if lang == "python":
|
| 86 |
+
io_obj = io.StringIO(source)
|
| 87 |
+
out = ""
|
| 88 |
+
prev_toktype = tokenize.INDENT
|
| 89 |
+
last_lineno = -1
|
| 90 |
+
last_col = 0
|
| 91 |
+
for tok in tokenize.generate_tokens(io_obj.readline):
|
| 92 |
+
token_type, token_string = tok[0], tok[1]
|
| 93 |
+
start_line, start_col = tok[2]
|
| 94 |
+
end_line, end_col = tok[3]
|
| 95 |
+
if start_line > last_lineno:
|
| 96 |
+
last_col = 0
|
| 97 |
+
if start_col > last_col:
|
| 98 |
+
out += " " * (start_col - last_col)
|
| 99 |
+
if token_type == tokenize.COMMENT:
|
| 100 |
+
pass
|
| 101 |
+
elif token_type == tokenize.STRING:
|
| 102 |
+
# A string that starts a logical line is a docstring -> drop it.
|
| 103 |
+
if prev_toktype != tokenize.INDENT and prev_toktype != tokenize.NEWLINE:
|
| 104 |
+
if start_col > 0:
|
| 105 |
+
out += token_string
|
| 106 |
+
else:
|
| 107 |
+
out += token_string
|
| 108 |
+
prev_toktype = token_type
|
| 109 |
+
last_col = end_col
|
| 110 |
+
last_lineno = end_line
|
| 111 |
+
return "\n".join(x for x in out.split("\n") if x.strip() != "")
|
| 112 |
+
|
| 113 |
+
def _replacer(match: re.Match[str]) -> str:
|
| 114 |
+
s = match.group(0)
|
| 115 |
+
return " " if s.startswith("/") else s
|
| 116 |
+
|
| 117 |
+
pattern = re.compile(
|
| 118 |
+
r"//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|\"(?:\\.|[^\\\"])*\"",
|
| 119 |
+
re.DOTALL | re.MULTILINE,
|
| 120 |
+
)
|
| 121 |
+
cleaned = re.sub(pattern, _replacer, source)
|
| 122 |
+
return "\n".join(x for x in cleaned.split("\n") if x.strip() != "")
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def tree_to_token_index(root_node: Node) -> list[tuple[Any, Any]]:
|
| 126 |
+
"""Collect ``(start_point, end_point)`` spans of every leaf token."""
|
| 127 |
+
if (len(root_node.children) == 0 or root_node.type == "string") and root_node.type != "comment":
|
| 128 |
+
return [(root_node.start_point, root_node.end_point)]
|
| 129 |
+
spans: list[tuple[Any, Any]] = []
|
| 130 |
+
for child in root_node.children:
|
| 131 |
+
spans += tree_to_token_index(child)
|
| 132 |
+
return spans
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def tree_to_variable_index(root_node: Node, index_to_code: dict) -> list[tuple[Any, Any]]:
|
| 136 |
+
"""Collect spans of leaves that are *variables* (token text != node type)."""
|
| 137 |
+
if (len(root_node.children) == 0 or root_node.type == "string") and root_node.type != "comment":
|
| 138 |
+
index = (root_node.start_point, root_node.end_point)
|
| 139 |
+
_, code = index_to_code[index]
|
| 140 |
+
return [] if root_node.type == code else [index]
|
| 141 |
+
spans: list[tuple[Any, Any]] = []
|
| 142 |
+
for child in root_node.children:
|
| 143 |
+
spans += tree_to_variable_index(child, index_to_code)
|
| 144 |
+
return spans
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def index_to_code_token(index: tuple[Any, Any], code: list[str]) -> str:
|
| 148 |
+
"""Slice the source text covered by a ``(start_point, end_point)`` span."""
|
| 149 |
+
start_point, end_point = index
|
| 150 |
+
if start_point[0] == end_point[0]:
|
| 151 |
+
return code[start_point[0]][start_point[1] : end_point[1]]
|
| 152 |
+
s = code[start_point[0]][start_point[1] :]
|
| 153 |
+
for i in range(start_point[0] + 1, end_point[0]):
|
| 154 |
+
s += code[i]
|
| 155 |
+
s += code[end_point[0]][: end_point[1]]
|
| 156 |
+
return s
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
# --------------------------------------------------------------------------- #
|
| 160 |
+
# parser/DFG.py :: DFG_python
|
| 161 |
+
# --------------------------------------------------------------------------- #
|
| 162 |
+
_ASSIGNMENT = ("assignment", "augmented_assignment", "for_in_clause")
|
| 163 |
+
_IF_STATEMENT = ("if_statement",)
|
| 164 |
+
_FOR_STATEMENT = ("for_statement",)
|
| 165 |
+
_WHILE_STATEMENT = ("while_statement",)
|
| 166 |
+
_DO_FIRST_STATEMENT = ("for_in_clause",)
|
| 167 |
+
_DEF_STATEMENT = ("default_parameter",)
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def DFG_python(root_node: Node, index_to_code: dict, states: dict) -> tuple[list, dict]:
|
| 171 |
+
"""Build the data-flow graph of a Python AST subtree.
|
| 172 |
+
|
| 173 |
+
Returns ``(dfg_edges, variable_states)`` where ``variable_states`` maps a
|
| 174 |
+
variable name to the token indices that currently define it.
|
| 175 |
+
"""
|
| 176 |
+
states = states.copy()
|
| 177 |
+
|
| 178 |
+
if (len(root_node.children) == 0 or root_node.type == "string") and root_node.type != "comment":
|
| 179 |
+
idx, code = index_to_code[(root_node.start_point, root_node.end_point)]
|
| 180 |
+
if root_node.type == code: # a keyword/operator, not a variable
|
| 181 |
+
return [], states
|
| 182 |
+
if code in states:
|
| 183 |
+
return [(code, idx, "comesFrom", [code], states[code].copy())], states
|
| 184 |
+
if root_node.type == "identifier":
|
| 185 |
+
states[code] = [idx]
|
| 186 |
+
return [(code, idx, "comesFrom", [], [])], states
|
| 187 |
+
|
| 188 |
+
if root_node.type in _DEF_STATEMENT:
|
| 189 |
+
name = root_node.child_by_field_name("name")
|
| 190 |
+
value = root_node.child_by_field_name("value")
|
| 191 |
+
dfg: list = []
|
| 192 |
+
if value is None:
|
| 193 |
+
for index in tree_to_variable_index(name, index_to_code):
|
| 194 |
+
idx, code = index_to_code[index]
|
| 195 |
+
dfg.append((code, idx, "comesFrom", [], []))
|
| 196 |
+
states[code] = [idx]
|
| 197 |
+
return sorted(dfg, key=lambda x: x[1]), states
|
| 198 |
+
name_indexs = tree_to_variable_index(name, index_to_code)
|
| 199 |
+
value_indexs = tree_to_variable_index(value, index_to_code)
|
| 200 |
+
temp, states = DFG_python(value, index_to_code, states)
|
| 201 |
+
dfg += temp
|
| 202 |
+
for index1 in name_indexs:
|
| 203 |
+
idx1, code1 = index_to_code[index1]
|
| 204 |
+
for index2 in value_indexs:
|
| 205 |
+
idx2, code2 = index_to_code[index2]
|
| 206 |
+
dfg.append((code1, idx1, "comesFrom", [code2], [idx2]))
|
| 207 |
+
states[code1] = [idx1]
|
| 208 |
+
return sorted(dfg, key=lambda x: x[1]), states
|
| 209 |
+
|
| 210 |
+
if root_node.type in _ASSIGNMENT:
|
| 211 |
+
if root_node.type == "for_in_clause":
|
| 212 |
+
right_nodes = [root_node.children[-1]]
|
| 213 |
+
left_nodes = [root_node.child_by_field_name("left")]
|
| 214 |
+
else:
|
| 215 |
+
if root_node.child_by_field_name("right") is None:
|
| 216 |
+
return [], states
|
| 217 |
+
left_nodes = [x for x in root_node.child_by_field_name("left").children if x.type != ","]
|
| 218 |
+
right_nodes = [
|
| 219 |
+
x for x in root_node.child_by_field_name("right").children if x.type != ","
|
| 220 |
+
]
|
| 221 |
+
if len(right_nodes) != len(left_nodes):
|
| 222 |
+
left_nodes = [root_node.child_by_field_name("left")]
|
| 223 |
+
right_nodes = [root_node.child_by_field_name("right")]
|
| 224 |
+
if len(left_nodes) == 0:
|
| 225 |
+
left_nodes = [root_node.child_by_field_name("left")]
|
| 226 |
+
if len(right_nodes) == 0:
|
| 227 |
+
right_nodes = [root_node.child_by_field_name("right")]
|
| 228 |
+
dfg = []
|
| 229 |
+
for node in right_nodes:
|
| 230 |
+
temp, states = DFG_python(node, index_to_code, states)
|
| 231 |
+
dfg += temp
|
| 232 |
+
for left_node, right_node in zip(left_nodes, right_nodes):
|
| 233 |
+
left_tokens_index = tree_to_variable_index(left_node, index_to_code)
|
| 234 |
+
right_tokens_index = tree_to_variable_index(right_node, index_to_code)
|
| 235 |
+
for token1_index in left_tokens_index:
|
| 236 |
+
idx1, code1 = index_to_code[token1_index]
|
| 237 |
+
dfg.append(
|
| 238 |
+
(
|
| 239 |
+
code1,
|
| 240 |
+
idx1,
|
| 241 |
+
"computedFrom",
|
| 242 |
+
[index_to_code[x][1] for x in right_tokens_index],
|
| 243 |
+
[index_to_code[x][0] for x in right_tokens_index],
|
| 244 |
+
)
|
| 245 |
+
)
|
| 246 |
+
states[code1] = [idx1]
|
| 247 |
+
return sorted(dfg, key=lambda x: x[1]), states
|
| 248 |
+
|
| 249 |
+
if root_node.type in _IF_STATEMENT:
|
| 250 |
+
dfg = []
|
| 251 |
+
current_states = states.copy()
|
| 252 |
+
others_states = []
|
| 253 |
+
tag = "else" in root_node.type
|
| 254 |
+
for child in root_node.children:
|
| 255 |
+
if "else" in child.type:
|
| 256 |
+
tag = True
|
| 257 |
+
if child.type not in ("elif_clause", "else_clause"):
|
| 258 |
+
temp, current_states = DFG_python(child, index_to_code, current_states)
|
| 259 |
+
dfg += temp
|
| 260 |
+
else:
|
| 261 |
+
temp, new_states = DFG_python(child, index_to_code, states)
|
| 262 |
+
dfg += temp
|
| 263 |
+
others_states.append(new_states)
|
| 264 |
+
others_states.append(current_states)
|
| 265 |
+
if tag is False:
|
| 266 |
+
others_states.append(states)
|
| 267 |
+
merged: dict = {}
|
| 268 |
+
for dic in others_states:
|
| 269 |
+
for key in dic:
|
| 270 |
+
merged.setdefault(key, [])
|
| 271 |
+
merged[key] += dic[key]
|
| 272 |
+
for key in merged:
|
| 273 |
+
merged[key] = sorted(set(merged[key]))
|
| 274 |
+
return sorted(dfg, key=lambda x: x[1]), merged
|
| 275 |
+
|
| 276 |
+
if root_node.type in _FOR_STATEMENT:
|
| 277 |
+
dfg = []
|
| 278 |
+
# Two passes: loop bodies can consume values defined later in the loop.
|
| 279 |
+
for _ in range(2):
|
| 280 |
+
right_nodes = [x for x in root_node.child_by_field_name("right").children if x.type != ","]
|
| 281 |
+
left_nodes = [x for x in root_node.child_by_field_name("left").children if x.type != ","]
|
| 282 |
+
if len(right_nodes) != len(left_nodes):
|
| 283 |
+
left_nodes = [root_node.child_by_field_name("left")]
|
| 284 |
+
right_nodes = [root_node.child_by_field_name("right")]
|
| 285 |
+
if len(left_nodes) == 0:
|
| 286 |
+
left_nodes = [root_node.child_by_field_name("left")]
|
| 287 |
+
if len(right_nodes) == 0:
|
| 288 |
+
right_nodes = [root_node.child_by_field_name("right")]
|
| 289 |
+
for node in right_nodes:
|
| 290 |
+
temp, states = DFG_python(node, index_to_code, states)
|
| 291 |
+
dfg += temp
|
| 292 |
+
for left_node, right_node in zip(left_nodes, right_nodes):
|
| 293 |
+
left_tokens_index = tree_to_variable_index(left_node, index_to_code)
|
| 294 |
+
right_tokens_index = tree_to_variable_index(right_node, index_to_code)
|
| 295 |
+
for token1_index in left_tokens_index:
|
| 296 |
+
idx1, code1 = index_to_code[token1_index]
|
| 297 |
+
dfg.append(
|
| 298 |
+
(
|
| 299 |
+
code1,
|
| 300 |
+
idx1,
|
| 301 |
+
"computedFrom",
|
| 302 |
+
[index_to_code[x][1] for x in right_tokens_index],
|
| 303 |
+
[index_to_code[x][0] for x in right_tokens_index],
|
| 304 |
+
)
|
| 305 |
+
)
|
| 306 |
+
states[code1] = [idx1]
|
| 307 |
+
if root_node.children[-1].type == "block":
|
| 308 |
+
temp, states = DFG_python(root_node.children[-1], index_to_code, states)
|
| 309 |
+
dfg += temp
|
| 310 |
+
return _merge_duplicate_edges(dfg), states
|
| 311 |
+
|
| 312 |
+
if root_node.type in _WHILE_STATEMENT:
|
| 313 |
+
dfg = []
|
| 314 |
+
for _ in range(2):
|
| 315 |
+
for child in root_node.children:
|
| 316 |
+
temp, states = DFG_python(child, index_to_code, states)
|
| 317 |
+
dfg += temp
|
| 318 |
+
return _merge_duplicate_edges(dfg), states
|
| 319 |
+
|
| 320 |
+
dfg = []
|
| 321 |
+
for child in root_node.children:
|
| 322 |
+
if child.type in _DO_FIRST_STATEMENT:
|
| 323 |
+
temp, states = DFG_python(child, index_to_code, states)
|
| 324 |
+
dfg += temp
|
| 325 |
+
for child in root_node.children:
|
| 326 |
+
if child.type not in _DO_FIRST_STATEMENT:
|
| 327 |
+
temp, states = DFG_python(child, index_to_code, states)
|
| 328 |
+
dfg += temp
|
| 329 |
+
return sorted(dfg, key=lambda x: x[1]), states
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
def _merge_duplicate_edges(dfg: list) -> list:
|
| 333 |
+
"""Collapse the duplicate edges produced by the two-pass loop handling."""
|
| 334 |
+
dic: dict = {}
|
| 335 |
+
for x in dfg:
|
| 336 |
+
key = (x[0], x[1], x[2])
|
| 337 |
+
if key not in dic:
|
| 338 |
+
dic[key] = [x[3], x[4]]
|
| 339 |
+
else:
|
| 340 |
+
dic[key][0] = list(set(dic[key][0] + x[3]))
|
| 341 |
+
dic[key][1] = sorted(set(dic[key][1] + x[4]))
|
| 342 |
+
merged = [(k[0], k[1], k[2], v[0], v[1]) for k, v in sorted(dic.items(), key=lambda t: t[0][1])]
|
| 343 |
+
return sorted(merged, key=lambda x: x[1])
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
# --------------------------------------------------------------------------- #
|
| 347 |
+
# Public entry point (GraphCodeBERT's `extract_dataflow`)
|
| 348 |
+
# --------------------------------------------------------------------------- #
|
| 349 |
+
def extract_dataflow(code: str, language: str = "python") -> tuple[list[str], list, dict]:
|
| 350 |
+
"""Tokenise ``code`` and extract its data-flow graph.
|
| 351 |
+
|
| 352 |
+
Returns ``(code_tokens, dfg, status)``. ``status`` records *why* a stage
|
| 353 |
+
degraded so callers can report it instead of hiding it:
|
| 354 |
+
|
| 355 |
+
``comment_strip`` : ``"ok"`` | ``"failed"``
|
| 356 |
+
``parse`` : ``"ok"`` | ``"failed"``
|
| 357 |
+
``dfg`` : ``"ok"`` | ``"failed"`` | ``"recursion_limit"``
|
| 358 |
+
``error`` : ``None`` or ``"<ExcType>: <message>"``
|
| 359 |
+
|
| 360 |
+
A degraded DFG yields an **empty** data-flow component -- the snippet is
|
| 361 |
+
still trained on (GraphCodeBERT tolerates zero nodes), it is never dropped.
|
| 362 |
+
"""
|
| 363 |
+
status: dict[str, Any] = {"comment_strip": "ok", "parse": "ok", "dfg": "ok", "error": None}
|
| 364 |
+
|
| 365 |
+
try:
|
| 366 |
+
cleaned = remove_comments_and_docstrings(code, language)
|
| 367 |
+
except Exception as exc:
|
| 368 |
+
# Syntactically broken snippets are common in the wild; fall back to the
|
| 369 |
+
# raw source rather than discarding the example.
|
| 370 |
+
status["comment_strip"] = "failed"
|
| 371 |
+
status["error"] = f"{type(exc).__name__}: {exc}"
|
| 372 |
+
cleaned = code
|
| 373 |
+
|
| 374 |
+
parser = get_parser(language)
|
| 375 |
+
try:
|
| 376 |
+
tree = parser.parse(bytes(cleaned, "utf8"))
|
| 377 |
+
root_node = tree.root_node
|
| 378 |
+
except Exception as exc:
|
| 379 |
+
raise DataFlowExtractionError(f"tree-sitter failed to parse snippet: {exc}") from exc
|
| 380 |
+
|
| 381 |
+
old_limit = sys.getrecursionlimit()
|
| 382 |
+
sys.setrecursionlimit(_RECURSION_LIMIT)
|
| 383 |
+
try:
|
| 384 |
+
try:
|
| 385 |
+
tokens_index = tree_to_token_index(root_node)
|
| 386 |
+
except RecursionError as exc:
|
| 387 |
+
status["parse"] = "failed"
|
| 388 |
+
status["dfg"] = "recursion_limit"
|
| 389 |
+
status["error"] = f"{type(exc).__name__}: token index recursion limit"
|
| 390 |
+
raise DataFlowExtractionError("snippet nests deeper than the recursion limit") from exc
|
| 391 |
+
|
| 392 |
+
lines = cleaned.split("\n")
|
| 393 |
+
code_tokens = [index_to_code_token(x, lines) for x in tokens_index]
|
| 394 |
+
index_to_code = {
|
| 395 |
+
index: (idx, token) for idx, (index, token) in enumerate(zip(tokens_index, code_tokens))
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
try:
|
| 399 |
+
dfg, _ = DFG_python(root_node, index_to_code, {})
|
| 400 |
+
except RecursionError as exc:
|
| 401 |
+
status["dfg"] = "recursion_limit"
|
| 402 |
+
status["error"] = f"{type(exc).__name__}: DFG recursion limit"
|
| 403 |
+
dfg = []
|
| 404 |
+
except Exception as exc:
|
| 405 |
+
status["dfg"] = "failed"
|
| 406 |
+
status["error"] = f"{type(exc).__name__}: {exc}"
|
| 407 |
+
dfg = []
|
| 408 |
+
finally:
|
| 409 |
+
sys.setrecursionlimit(old_limit)
|
| 410 |
+
|
| 411 |
+
# Keep only nodes that participate in at least one edge (GraphCodeBERT does
|
| 412 |
+
# the same: isolated nodes carry no data-flow signal).
|
| 413 |
+
dfg = sorted(dfg, key=lambda x: x[1])
|
| 414 |
+
keep: set[int] = set()
|
| 415 |
+
for d in dfg:
|
| 416 |
+
if len(d[-1]) != 0:
|
| 417 |
+
keep.add(d[1])
|
| 418 |
+
keep.update(d[-1])
|
| 419 |
+
dfg = [d for d in dfg if d[1] in keep]
|
| 420 |
+
|
| 421 |
+
return code_tokens, dfg, status
|
code/modeling.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""GraphCodeBERT clone-detection model.
|
| 2 |
+
|
| 3 |
+
Reimplements the architecture from Microsoft's ``GraphCodeBERT/clonedetection``
|
| 4 |
+
on top of ``transformers`` v5:
|
| 5 |
+
|
| 6 |
+
* the two snippets are encoded **separately** by one shared GraphCodeBERT
|
| 7 |
+
encoder, each with its own graph-guided masked attention;
|
| 8 |
+
* a data-flow node's input embedding is the average of the embeddings of the
|
| 9 |
+
code tokens it was identified from;
|
| 10 |
+
* the two ``<s>`` representations are concatenated and fed to a
|
| 11 |
+
``Linear(2H -> H) -> tanh -> Linear(H -> 2)`` head.
|
| 12 |
+
|
| 13 |
+
The only real adaptation is the attention mask: ``transformers`` v5 builds masks
|
| 14 |
+
through ``masking_utils`` and only forwards a mask untouched when it is already
|
| 15 |
+
4-D, so the boolean ``[B, L, L]`` graph mask is expanded to an additive
|
| 16 |
+
``[B, 1, L, L]`` mask here.
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
from __future__ import annotations
|
| 20 |
+
|
| 21 |
+
import torch
|
| 22 |
+
import torch.nn as nn
|
| 23 |
+
from transformers import RobertaConfig, RobertaModel, RobertaPreTrainedModel
|
| 24 |
+
from transformers.modeling_outputs import SequenceClassifierOutput
|
| 25 |
+
|
| 26 |
+
__all__ = ["GraphCodeBERTForCloneDetection", "CloneClassificationHead"]
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _autocast_dtype(device: torch.device, fallback: torch.dtype) -> torch.dtype:
|
| 30 |
+
"""Dtype the attention scores will actually have, honouring autocast.
|
| 31 |
+
|
| 32 |
+
SDPA requires an additive ``attn_mask`` whose dtype matches the query, so a
|
| 33 |
+
hard-coded float32 mask would break under ``fp16=True``.
|
| 34 |
+
"""
|
| 35 |
+
try:
|
| 36 |
+
if torch.is_autocast_enabled(device.type):
|
| 37 |
+
return torch.get_autocast_dtype(device.type)
|
| 38 |
+
except TypeError: # older signature without a device argument
|
| 39 |
+
if device.type == "cuda" and torch.is_autocast_enabled():
|
| 40 |
+
return torch.get_autocast_gpu_dtype()
|
| 41 |
+
return fallback
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _to_additive_mask(bool_mask: torch.Tensor, dtype: torch.dtype) -> torch.Tensor:
|
| 45 |
+
"""``[B, L, L]`` boolean -> ``[B, 1, L, L]`` additive mask (0 / -inf)."""
|
| 46 |
+
additive = torch.zeros(bool_mask.shape, dtype=dtype, device=bool_mask.device)
|
| 47 |
+
additive.masked_fill_(~bool_mask, torch.finfo(dtype).min)
|
| 48 |
+
return additive.unsqueeze(1)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class CloneClassificationHead(nn.Module):
|
| 52 |
+
"""Pairwise head over the two ``<s>`` vectors (GraphCodeBERT's own head)."""
|
| 53 |
+
|
| 54 |
+
def __init__(self, config: RobertaConfig) -> None:
|
| 55 |
+
super().__init__()
|
| 56 |
+
self.dense = nn.Linear(config.hidden_size * 2, config.hidden_size)
|
| 57 |
+
self.dropout = nn.Dropout(config.hidden_dropout_prob)
|
| 58 |
+
self.out_proj = nn.Linear(config.hidden_size, 2)
|
| 59 |
+
|
| 60 |
+
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
| 61 |
+
"""``hidden_states``: ``[B*2, L, H]`` -> logits ``[B, 2]``."""
|
| 62 |
+
x = hidden_states[:, 0, :] # <s> of each snippet
|
| 63 |
+
x = x.reshape(-1, x.size(-1) * 2) # pair the two snippets back up
|
| 64 |
+
x = self.dropout(x)
|
| 65 |
+
x = torch.tanh(self.dense(x))
|
| 66 |
+
x = self.dropout(x)
|
| 67 |
+
return self.out_proj(x)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class GraphCodeBERTForCloneDetection(RobertaPreTrainedModel):
|
| 71 |
+
"""Binary clone classifier: ``0 = not clone``, ``1 = clone``."""
|
| 72 |
+
|
| 73 |
+
config_class = RobertaConfig
|
| 74 |
+
base_model_prefix = "roberta"
|
| 75 |
+
supports_gradient_checkpointing = True
|
| 76 |
+
|
| 77 |
+
def __init__(self, config: RobertaConfig, class_weights: list[float] | None = None) -> None:
|
| 78 |
+
super().__init__(config)
|
| 79 |
+
config.num_labels = 2
|
| 80 |
+
self.roberta = RobertaModel(config, add_pooling_layer=False)
|
| 81 |
+
self.classifier = CloneClassificationHead(config)
|
| 82 |
+
self.register_buffer(
|
| 83 |
+
"class_weights",
|
| 84 |
+
torch.tensor(class_weights, dtype=torch.float32) if class_weights else None,
|
| 85 |
+
persistent=False,
|
| 86 |
+
)
|
| 87 |
+
self.post_init()
|
| 88 |
+
|
| 89 |
+
# ------------------------------------------------------------------ #
|
| 90 |
+
def _embed_with_dataflow(
|
| 91 |
+
self, input_ids: torch.Tensor, position_idx: torch.Tensor, attn_mask: torch.Tensor
|
| 92 |
+
) -> torch.Tensor:
|
| 93 |
+
"""Word embeddings where each data-flow node averages its code tokens.
|
| 94 |
+
|
| 95 |
+
``position_idx`` encodes the role of every slot: ``0`` = data-flow node,
|
| 96 |
+
``1`` (= ``<pad>``) = padding, ``>= 2`` = real code token.
|
| 97 |
+
"""
|
| 98 |
+
nodes_mask = position_idx.eq(0)
|
| 99 |
+
token_mask = position_idx.ge(2)
|
| 100 |
+
|
| 101 |
+
embeddings = self.roberta.embeddings.word_embeddings(input_ids)
|
| 102 |
+
# For every node row, the code-token columns it may look at.
|
| 103 |
+
nodes_to_token = nodes_mask[:, :, None] & token_mask[:, None, :] & attn_mask
|
| 104 |
+
nodes_to_token = nodes_to_token.to(embeddings.dtype)
|
| 105 |
+
nodes_to_token = nodes_to_token / (nodes_to_token.sum(-1) + 1e-10)[:, :, None]
|
| 106 |
+
averaged = torch.einsum("abc,acd->abd", nodes_to_token, embeddings)
|
| 107 |
+
return embeddings * (~nodes_mask)[:, :, None] + averaged * nodes_mask[:, :, None]
|
| 108 |
+
|
| 109 |
+
def _encode(
|
| 110 |
+
self, input_ids: torch.Tensor, position_idx: torch.Tensor, attn_mask: torch.Tensor
|
| 111 |
+
) -> torch.Tensor:
|
| 112 |
+
embeddings = self._embed_with_dataflow(input_ids, position_idx, attn_mask)
|
| 113 |
+
dtype = _autocast_dtype(input_ids.device, embeddings.dtype)
|
| 114 |
+
outputs = self.roberta(
|
| 115 |
+
inputs_embeds=embeddings,
|
| 116 |
+
attention_mask=_to_additive_mask(attn_mask, dtype),
|
| 117 |
+
position_ids=position_idx,
|
| 118 |
+
token_type_ids=torch.zeros_like(position_idx),
|
| 119 |
+
)
|
| 120 |
+
return outputs.last_hidden_state
|
| 121 |
+
|
| 122 |
+
# ------------------------------------------------------------------ #
|
| 123 |
+
def forward(
|
| 124 |
+
self,
|
| 125 |
+
input_ids_1: torch.Tensor,
|
| 126 |
+
position_idx_1: torch.Tensor,
|
| 127 |
+
attn_mask_1: torch.Tensor,
|
| 128 |
+
input_ids_2: torch.Tensor,
|
| 129 |
+
position_idx_2: torch.Tensor,
|
| 130 |
+
attn_mask_2: torch.Tensor,
|
| 131 |
+
labels: torch.Tensor | None = None,
|
| 132 |
+
) -> SequenceClassifierOutput:
|
| 133 |
+
"""Encode both snippets with the shared encoder and classify the pair.
|
| 134 |
+
|
| 135 |
+
Args:
|
| 136 |
+
input_ids_*: ``[B, L]`` token ids; data-flow slots hold ``<unk>``.
|
| 137 |
+
position_idx_*: ``[B, L]`` role/position ids (see ``_embed_with_dataflow``).
|
| 138 |
+
attn_mask_*: ``[B, L, L]`` boolean graph-guided attention mask.
|
| 139 |
+
labels: ``[B]`` with values in ``{0, 1}``.
|
| 140 |
+
"""
|
| 141 |
+
batch_size, seq_len = input_ids_1.shape
|
| 142 |
+
# Stack both snippets into one encoder call: [B, L] x2 -> [B*2, L].
|
| 143 |
+
input_ids = torch.cat((input_ids_1[:, None], input_ids_2[:, None]), 1).view(-1, seq_len)
|
| 144 |
+
position_idx = torch.cat((position_idx_1[:, None], position_idx_2[:, None]), 1).view(
|
| 145 |
+
-1, seq_len
|
| 146 |
+
)
|
| 147 |
+
attn_mask = torch.cat((attn_mask_1[:, None], attn_mask_2[:, None]), 1).view(
|
| 148 |
+
-1, seq_len, seq_len
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
hidden = self._encode(input_ids, position_idx, attn_mask)
|
| 152 |
+
logits = self.classifier(hidden)
|
| 153 |
+
|
| 154 |
+
loss = None
|
| 155 |
+
if labels is not None:
|
| 156 |
+
weight = None
|
| 157 |
+
if self.class_weights is not None:
|
| 158 |
+
weight = self.class_weights.to(device=logits.device, dtype=logits.dtype)
|
| 159 |
+
loss = nn.functional.cross_entropy(logits, labels.view(-1), weight=weight)
|
| 160 |
+
|
| 161 |
+
return SequenceClassifierOutput(loss=loss, logits=logits)
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def load_model(
|
| 165 |
+
model_name_or_path: str,
|
| 166 |
+
attn_implementation: str = "sdpa",
|
| 167 |
+
class_weights: list[float] | None = None,
|
| 168 |
+
gradient_checkpointing: bool = False,
|
| 169 |
+
) -> GraphCodeBERTForCloneDetection:
|
| 170 |
+
"""Load GraphCodeBERT weights into the pairwise clone-detection head."""
|
| 171 |
+
model = GraphCodeBERTForCloneDetection.from_pretrained(
|
| 172 |
+
model_name_or_path,
|
| 173 |
+
attn_implementation=attn_implementation,
|
| 174 |
+
)
|
| 175 |
+
# Set after loading: `from_pretrained` should not have to carry runtime-only
|
| 176 |
+
# arguments, and the weights are a training artefact, not part of the config.
|
| 177 |
+
model.class_weights = (
|
| 178 |
+
torch.tensor(class_weights, dtype=torch.float32) if class_weights else None
|
| 179 |
+
)
|
| 180 |
+
if model.config.model_type != "roberta":
|
| 181 |
+
raise ValueError(
|
| 182 |
+
f"Expected a RoBERTa-architecture checkpoint (GraphCodeBERT), "
|
| 183 |
+
f"got model_type={model.config.model_type!r}."
|
| 184 |
+
)
|
| 185 |
+
if gradient_checkpointing:
|
| 186 |
+
model.gradient_checkpointing_enable(gradient_checkpointing_kwargs={"use_reentrant": False})
|
| 187 |
+
return model
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def count_parameters(model: nn.Module) -> dict[str, int]:
|
| 191 |
+
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
|
| 192 |
+
total = sum(p.numel() for p in model.parameters())
|
| 193 |
+
return {"trainable_parameters": trainable, "total_parameters": total}
|
code/preprocess.py
ADDED
|
@@ -0,0 +1,839 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Dataset loading, leakage-safe splitting and GraphCodeBERT feature building.
|
| 2 |
+
|
| 3 |
+
Design notes
|
| 4 |
+
------------
|
| 5 |
+
The PoolC fold contains 6.7 M *pairs* but only ~45 k *distinct snippets* -- the
|
| 6 |
+
pairs are combinations of a small pool of solutions. So the expensive work
|
| 7 |
+
(tree-sitter parsing, data-flow extraction, BPE tokenisation) is done **once per
|
| 8 |
+
distinct snippet** and cached on disk; a pair is then just two integer indices
|
| 9 |
+
plus a label. Nothing proportional to 6.7 M rows is ever tokenised, and the
|
| 10 |
+
graph-guided attention masks are materialised lazily in the collator.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import hashlib
|
| 16 |
+
import json
|
| 17 |
+
import logging
|
| 18 |
+
import time
|
| 19 |
+
from collections import Counter
|
| 20 |
+
from dataclasses import dataclass
|
| 21 |
+
from pathlib import Path
|
| 22 |
+
from typing import Any, Iterator
|
| 23 |
+
|
| 24 |
+
import numpy as np
|
| 25 |
+
import pyarrow.parquet as pq
|
| 26 |
+
import torch
|
| 27 |
+
from torch.utils.data import Dataset
|
| 28 |
+
|
| 29 |
+
from config import (
|
| 30 |
+
CODE1_COLUMN,
|
| 31 |
+
CODE2_COLUMN,
|
| 32 |
+
DATASET_LANGUAGE,
|
| 33 |
+
FORBIDDEN_FEATURE_COLUMNS,
|
| 34 |
+
GROUP1_COLUMN,
|
| 35 |
+
GROUP2_COLUMN,
|
| 36 |
+
LABEL_COLUMN,
|
| 37 |
+
Config,
|
| 38 |
+
)
|
| 39 |
+
from dfg_parser import DataFlowExtractionError, extract_dataflow
|
| 40 |
+
|
| 41 |
+
logger = logging.getLogger(__name__)
|
| 42 |
+
|
| 43 |
+
_META_COLUMNS = [LABEL_COLUMN, GROUP1_COLUMN, GROUP2_COLUMN]
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
# --------------------------------------------------------------------------- #
|
| 47 |
+
# 1. Schema verification
|
| 48 |
+
# --------------------------------------------------------------------------- #
|
| 49 |
+
def verify_dataset_schema(dataset_name: str) -> dict[str, Any]:
|
| 50 |
+
"""Check the remote dataset still matches what this pipeline expects.
|
| 51 |
+
|
| 52 |
+
Raises immediately (rather than silently mis-reading columns) if the schema
|
| 53 |
+
drifts. Returns a report dict for the experiment log.
|
| 54 |
+
"""
|
| 55 |
+
from datasets import load_dataset_builder
|
| 56 |
+
|
| 57 |
+
builder = load_dataset_builder(dataset_name)
|
| 58 |
+
features = builder.info.features
|
| 59 |
+
splits = {k: v.num_examples for k, v in (builder.info.splits or {}).items()}
|
| 60 |
+
|
| 61 |
+
missing = [
|
| 62 |
+
c
|
| 63 |
+
for c in (CODE1_COLUMN, CODE2_COLUMN, LABEL_COLUMN, GROUP1_COLUMN, GROUP2_COLUMN)
|
| 64 |
+
if c not in features
|
| 65 |
+
]
|
| 66 |
+
if missing:
|
| 67 |
+
raise ValueError(
|
| 68 |
+
f"{dataset_name} is missing expected columns {missing}. "
|
| 69 |
+
f"Available: {sorted(features)}. Adapt config.py before continuing."
|
| 70 |
+
)
|
| 71 |
+
for col in (CODE1_COLUMN, CODE2_COLUMN):
|
| 72 |
+
if features[col].dtype != "string":
|
| 73 |
+
raise ValueError(f"Column {col!r} must be a string, got {features[col]}.")
|
| 74 |
+
|
| 75 |
+
report = {
|
| 76 |
+
"dataset_name": dataset_name,
|
| 77 |
+
"features": {k: str(v) for k, v in features.items()},
|
| 78 |
+
"splits": splits,
|
| 79 |
+
"forbidden_feature_columns": list(FORBIDDEN_FEATURE_COLUMNS),
|
| 80 |
+
"language": DATASET_LANGUAGE,
|
| 81 |
+
}
|
| 82 |
+
logger.info("Dataset schema OK: %s", json.dumps(report["splits"]))
|
| 83 |
+
return report
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def _parquet_files(dataset_name: str, split: str) -> list[str]:
|
| 87 |
+
"""Resolve the local parquet shards for one split (downloads on first use)."""
|
| 88 |
+
from huggingface_hub import snapshot_download
|
| 89 |
+
|
| 90 |
+
root = Path(
|
| 91 |
+
snapshot_download(dataset_name, repo_type="dataset", allow_patterns=["data/*", "*.json"])
|
| 92 |
+
)
|
| 93 |
+
files = sorted(root.glob(f"data/{split}-*.parquet"))
|
| 94 |
+
if not files:
|
| 95 |
+
files = sorted(root.glob(f"**/{split}-*.parquet"))
|
| 96 |
+
if not files:
|
| 97 |
+
raise FileNotFoundError(
|
| 98 |
+
f"No parquet shards for split {split!r} under {root}. "
|
| 99 |
+
f"Found: {[p.name for p in root.rglob('*.parquet')]}"
|
| 100 |
+
)
|
| 101 |
+
return [str(p) for p in files]
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
# --------------------------------------------------------------------------- #
|
| 105 |
+
# 2. Snippet pool + pair index (cached)
|
| 106 |
+
# --------------------------------------------------------------------------- #
|
| 107 |
+
@dataclass
|
| 108 |
+
class SplitIndex:
|
| 109 |
+
"""A split reduced to integer indices into the shared snippet pool."""
|
| 110 |
+
|
| 111 |
+
name: str
|
| 112 |
+
snippet_id1: np.ndarray # int32 [n_pairs]
|
| 113 |
+
snippet_id2: np.ndarray # int32 [n_pairs]
|
| 114 |
+
labels: np.ndarray # int8 [n_pairs]
|
| 115 |
+
group1: np.ndarray # int32 [n_pairs]
|
| 116 |
+
group2: np.ndarray # int32 [n_pairs]
|
| 117 |
+
|
| 118 |
+
def __len__(self) -> int:
|
| 119 |
+
return int(self.labels.shape[0])
|
| 120 |
+
|
| 121 |
+
def select(self, rows: np.ndarray, name: str | None = None) -> "SplitIndex":
|
| 122 |
+
return SplitIndex(
|
| 123 |
+
name=name or self.name,
|
| 124 |
+
snippet_id1=self.snippet_id1[rows],
|
| 125 |
+
snippet_id2=self.snippet_id2[rows],
|
| 126 |
+
labels=self.labels[rows],
|
| 127 |
+
group1=self.group1[rows],
|
| 128 |
+
group2=self.group2[rows],
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
def class_distribution(self) -> dict[str, Any]:
|
| 132 |
+
counts = Counter(self.labels.tolist())
|
| 133 |
+
n = max(len(self), 1)
|
| 134 |
+
return {
|
| 135 |
+
"num_examples": len(self),
|
| 136 |
+
"negatives_label_0": int(counts.get(0, 0)),
|
| 137 |
+
"positives_label_1": int(counts.get(1, 0)),
|
| 138 |
+
"positive_ratio": round(counts.get(1, 0) / n, 6),
|
| 139 |
+
"num_groups": int(np.unique(np.concatenate([self.group1, self.group2])).size),
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
def snippet_ids(self) -> np.ndarray:
|
| 143 |
+
return np.unique(np.concatenate([self.snippet_id1, self.snippet_id2]))
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def _hash_code(text: str) -> bytes:
|
| 147 |
+
return hashlib.blake2b(text.encode("utf-8", "ignore"), digest_size=16).digest()
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def build_snippet_pool(
|
| 151 |
+
cfg: Config, splits: tuple[str, ...]
|
| 152 |
+
) -> tuple[list[str], dict[str, SplitIndex], dict[str, Any]]:
|
| 153 |
+
"""Deduplicate every snippet across ``splits`` and index the pairs.
|
| 154 |
+
|
| 155 |
+
Cached under ``cfg.cache_dir`` -- the scan over the parquet shards is the
|
| 156 |
+
only pass that ever touches all 6.7 M rows, and it runs once.
|
| 157 |
+
"""
|
| 158 |
+
cache = Path(cfg.cache_dir) / "pairs" / _fingerprint(cfg.dataset_name, splits)
|
| 159 |
+
if (cache / "meta.json").exists():
|
| 160 |
+
logger.info("Reusing cached snippet pool at %s", cache)
|
| 161 |
+
return _load_pool(cache)
|
| 162 |
+
|
| 163 |
+
cache.mkdir(parents=True, exist_ok=True)
|
| 164 |
+
t0 = time.time()
|
| 165 |
+
code_to_id: dict[bytes, int] = {}
|
| 166 |
+
snippets: list[str] = []
|
| 167 |
+
indices: dict[str, SplitIndex] = {}
|
| 168 |
+
#: A snippet appearing under two different group ids is a (rare) duplicate
|
| 169 |
+
#: solution; we log it because it is the only within-split ambiguity.
|
| 170 |
+
snippet_groups: dict[int, set[int]] = {}
|
| 171 |
+
|
| 172 |
+
for split in splits:
|
| 173 |
+
files = _parquet_files(cfg.dataset_name, split)
|
| 174 |
+
id1_chunks, id2_chunks, lab_chunks, g1_chunks, g2_chunks = [], [], [], [], []
|
| 175 |
+
for path in files:
|
| 176 |
+
pf = pq.ParquetFile(path)
|
| 177 |
+
for batch in pf.iter_batches(
|
| 178 |
+
batch_size=50_000,
|
| 179 |
+
columns=[CODE1_COLUMN, CODE2_COLUMN, *_META_COLUMNS],
|
| 180 |
+
):
|
| 181 |
+
cols = batch.to_pydict()
|
| 182 |
+
for code_col, group_col, out in (
|
| 183 |
+
(CODE1_COLUMN, GROUP1_COLUMN, id1_chunks),
|
| 184 |
+
(CODE2_COLUMN, GROUP2_COLUMN, id2_chunks),
|
| 185 |
+
):
|
| 186 |
+
ids = np.empty(len(cols[code_col]), dtype=np.int32)
|
| 187 |
+
for i, (text, group) in enumerate(zip(cols[code_col], cols[group_col])):
|
| 188 |
+
key = _hash_code(text)
|
| 189 |
+
sid = code_to_id.get(key)
|
| 190 |
+
if sid is None:
|
| 191 |
+
sid = len(snippets)
|
| 192 |
+
code_to_id[key] = sid
|
| 193 |
+
snippets.append(text)
|
| 194 |
+
ids[i] = sid
|
| 195 |
+
snippet_groups.setdefault(sid, set()).add(int(group))
|
| 196 |
+
out.append(ids)
|
| 197 |
+
lab_chunks.append(np.asarray(cols[LABEL_COLUMN], dtype=np.int8))
|
| 198 |
+
g1_chunks.append(np.asarray(cols[GROUP1_COLUMN], dtype=np.int32))
|
| 199 |
+
g2_chunks.append(np.asarray(cols[GROUP2_COLUMN], dtype=np.int32))
|
| 200 |
+
logger.info(" scanned %s", Path(path).name)
|
| 201 |
+
|
| 202 |
+
idx = SplitIndex(
|
| 203 |
+
name=split,
|
| 204 |
+
snippet_id1=np.concatenate(id1_chunks),
|
| 205 |
+
snippet_id2=np.concatenate(id2_chunks),
|
| 206 |
+
labels=np.concatenate(lab_chunks),
|
| 207 |
+
group1=np.concatenate(g1_chunks),
|
| 208 |
+
group2=np.concatenate(g2_chunks),
|
| 209 |
+
)
|
| 210 |
+
_assert_label_matches_groups(idx)
|
| 211 |
+
indices[split] = idx
|
| 212 |
+
logger.info("Split %s: %s", split, json.dumps(idx.class_distribution()))
|
| 213 |
+
|
| 214 |
+
ambiguous = sorted(sid for sid, gs in snippet_groups.items() if len(gs) > 1)
|
| 215 |
+
leakage = _cross_split_leakage(indices)
|
| 216 |
+
stats = {
|
| 217 |
+
"num_unique_snippets": len(snippets),
|
| 218 |
+
"scan_seconds": round(time.time() - t0, 1),
|
| 219 |
+
"snippets_with_multiple_groups": len(ambiguous),
|
| 220 |
+
"cross_split_snippet_overlap": leakage,
|
| 221 |
+
"per_split": {k: v.class_distribution() for k, v in indices.items()},
|
| 222 |
+
}
|
| 223 |
+
_save_pool(cache, snippets, indices, stats)
|
| 224 |
+
logger.info("Snippet pool: %s", json.dumps(stats, indent=2))
|
| 225 |
+
return snippets, indices, stats
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
def _assert_label_matches_groups(idx: SplitIndex) -> None:
|
| 229 |
+
"""The label is exactly ``group1 == group2``; assert it and shout about it.
|
| 230 |
+
|
| 231 |
+
This is why ``code1_group``/``code2_group`` are on the forbidden list: they
|
| 232 |
+
are a perfect proxy for the target.
|
| 233 |
+
"""
|
| 234 |
+
implied = (idx.group1 == idx.group2).astype(np.int8)
|
| 235 |
+
mismatches = int((implied != idx.labels).sum())
|
| 236 |
+
if mismatches:
|
| 237 |
+
raise ValueError(
|
| 238 |
+
f"Split {idx.name}: {mismatches} rows where `similar` disagrees with "
|
| 239 |
+
"(code1_group == code2_group). The dataset changed; revisit the split logic."
|
| 240 |
+
)
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
def _cross_split_leakage(indices: dict[str, SplitIndex]) -> dict[str, int]:
|
| 244 |
+
"""Count snippets and groups shared between splits (must be zero)."""
|
| 245 |
+
out: dict[str, int] = {}
|
| 246 |
+
names = list(indices)
|
| 247 |
+
for i, a in enumerate(names):
|
| 248 |
+
for b in names[i + 1 :]:
|
| 249 |
+
sa, sb = set(indices[a].snippet_ids().tolist()), set(indices[b].snippet_ids().tolist())
|
| 250 |
+
ga = set(np.unique(np.concatenate([indices[a].group1, indices[a].group2])).tolist())
|
| 251 |
+
gb = set(np.unique(np.concatenate([indices[b].group1, indices[b].group2])).tolist())
|
| 252 |
+
out[f"{a}|{b}:snippets"] = len(sa & sb)
|
| 253 |
+
out[f"{a}|{b}:groups"] = len(ga & gb)
|
| 254 |
+
return out
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
def _fingerprint(*parts: Any) -> str:
|
| 258 |
+
return hashlib.blake2b(repr(parts).encode(), digest_size=8).hexdigest()
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def _save_pool(
|
| 262 |
+
cache: Path, snippets: list[str], indices: dict[str, SplitIndex], stats: dict
|
| 263 |
+
) -> None:
|
| 264 |
+
import pyarrow as pa
|
| 265 |
+
|
| 266 |
+
pq.write_table(pa.table({"code": snippets}), cache / "snippets.parquet")
|
| 267 |
+
for name, idx in indices.items():
|
| 268 |
+
np.savez(
|
| 269 |
+
cache / f"{name}.npz",
|
| 270 |
+
snippet_id1=idx.snippet_id1,
|
| 271 |
+
snippet_id2=idx.snippet_id2,
|
| 272 |
+
labels=idx.labels,
|
| 273 |
+
group1=idx.group1,
|
| 274 |
+
group2=idx.group2,
|
| 275 |
+
)
|
| 276 |
+
(cache / "meta.json").write_text(
|
| 277 |
+
json.dumps({"splits": list(indices), "stats": stats}, indent=2), encoding="utf-8"
|
| 278 |
+
)
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
def _load_pool(cache: Path) -> tuple[list[str], dict[str, SplitIndex], dict[str, Any]]:
|
| 282 |
+
meta = json.loads((cache / "meta.json").read_text(encoding="utf-8"))
|
| 283 |
+
snippets = pq.read_table(cache / "snippets.parquet").column("code").to_pylist()
|
| 284 |
+
indices = {}
|
| 285 |
+
for name in meta["splits"]:
|
| 286 |
+
z = np.load(cache / f"{name}.npz")
|
| 287 |
+
indices[name] = SplitIndex(
|
| 288 |
+
name=name,
|
| 289 |
+
snippet_id1=z["snippet_id1"],
|
| 290 |
+
snippet_id2=z["snippet_id2"],
|
| 291 |
+
labels=z["labels"],
|
| 292 |
+
group1=z["group1"],
|
| 293 |
+
group2=z["group2"],
|
| 294 |
+
)
|
| 295 |
+
return snippets, indices, meta["stats"]
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
# --------------------------------------------------------------------------- #
|
| 299 |
+
# 3. Leakage-safe splitting
|
| 300 |
+
# --------------------------------------------------------------------------- #
|
| 301 |
+
def split_heldout_by_group(
|
| 302 |
+
heldout: SplitIndex, test_group_fraction: float, seed: int
|
| 303 |
+
) -> tuple[SplitIndex, SplitIndex, dict[str, Any]]:
|
| 304 |
+
"""Partition the held-out split into validation/test along *group* boundaries.
|
| 305 |
+
|
| 306 |
+
The dataset ships only ``train`` and ``val``; ``val`` is carved into a
|
| 307 |
+
validation and a test half by assigning whole problem groups to one side.
|
| 308 |
+
Pairs whose two snippets straddle the boundary are dropped -- keeping them
|
| 309 |
+
would put the same group on both sides.
|
| 310 |
+
"""
|
| 311 |
+
groups = np.unique(np.concatenate([heldout.group1, heldout.group2]))
|
| 312 |
+
rng = np.random.default_rng(seed)
|
| 313 |
+
shuffled = groups.copy()
|
| 314 |
+
rng.shuffle(shuffled)
|
| 315 |
+
n_test = max(1, int(round(len(shuffled) * test_group_fraction)))
|
| 316 |
+
if n_test >= len(shuffled):
|
| 317 |
+
raise ValueError("test_group_fraction leaves no groups for validation.")
|
| 318 |
+
test_groups = set(shuffled[:n_test].tolist())
|
| 319 |
+
val_groups = set(shuffled[n_test:].tolist())
|
| 320 |
+
|
| 321 |
+
in_test = np.isin(heldout.group1, list(test_groups)) & np.isin(
|
| 322 |
+
heldout.group2, list(test_groups)
|
| 323 |
+
)
|
| 324 |
+
in_val = np.isin(heldout.group1, list(val_groups)) & np.isin(heldout.group2, list(val_groups))
|
| 325 |
+
dropped = int(len(heldout) - in_test.sum() - in_val.sum())
|
| 326 |
+
|
| 327 |
+
validation = heldout.select(np.flatnonzero(in_val), name="validation")
|
| 328 |
+
test = heldout.select(np.flatnonzero(in_test), name="test")
|
| 329 |
+
|
| 330 |
+
overlap = set(validation.snippet_ids().tolist()) & set(test.snippet_ids().tolist())
|
| 331 |
+
if overlap:
|
| 332 |
+
raise AssertionError(f"{len(overlap)} snippets leaked between validation and test.")
|
| 333 |
+
|
| 334 |
+
report = {
|
| 335 |
+
"heldout_groups": int(len(groups)),
|
| 336 |
+
"validation_groups": len(val_groups),
|
| 337 |
+
"test_groups": len(test_groups),
|
| 338 |
+
"dropped_cross_boundary_pairs": dropped,
|
| 339 |
+
"validation": validation.class_distribution(),
|
| 340 |
+
"test": test.class_distribution(),
|
| 341 |
+
}
|
| 342 |
+
return validation, test, report
|
| 343 |
+
|
| 344 |
+
|
| 345 |
+
def subsample(
|
| 346 |
+
idx: SplitIndex, max_samples: int, seed: int, balanced: bool = True
|
| 347 |
+
) -> tuple[SplitIndex, dict[str, Any]]:
|
| 348 |
+
"""Take at most ``max_samples`` rows, optionally keeping the classes balanced.
|
| 349 |
+
|
| 350 |
+
Subsampling never crosses group boundaries (it only removes rows), so it
|
| 351 |
+
cannot introduce leakage.
|
| 352 |
+
"""
|
| 353 |
+
if max_samples < 0 or max_samples >= len(idx):
|
| 354 |
+
return idx, {"subsampled": False, "kept": len(idx)}
|
| 355 |
+
|
| 356 |
+
rng = np.random.default_rng(seed)
|
| 357 |
+
if balanced:
|
| 358 |
+
per_class = max_samples // 2
|
| 359 |
+
chosen = []
|
| 360 |
+
for label in (0, 1):
|
| 361 |
+
rows = np.flatnonzero(idx.labels == label)
|
| 362 |
+
take = min(per_class, len(rows))
|
| 363 |
+
chosen.append(rng.choice(rows, size=take, replace=False))
|
| 364 |
+
rows = np.sort(np.concatenate(chosen))
|
| 365 |
+
else:
|
| 366 |
+
rows = np.sort(rng.choice(len(idx), size=max_samples, replace=False))
|
| 367 |
+
|
| 368 |
+
out = idx.select(rows)
|
| 369 |
+
return out, {"subsampled": True, "kept": len(out), "balanced": balanced}
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
def decide_class_weights(
|
| 373 |
+
train: SplitIndex, mode: str, threshold: float
|
| 374 |
+
) -> tuple[list[float] | None, dict[str, Any]]:
|
| 375 |
+
"""Decide whether class-weighted cross entropy is warranted.
|
| 376 |
+
|
| 377 |
+
Weighting is *not* applied by default: it is enabled only when the measured
|
| 378 |
+
majority-class share exceeds ``threshold``. The rationale is recorded in the
|
| 379 |
+
returned report and written to ``training_config.json``.
|
| 380 |
+
"""
|
| 381 |
+
dist = train.class_distribution()
|
| 382 |
+
n0, n1 = dist["negatives_label_0"], dist["positives_label_1"]
|
| 383 |
+
total = max(n0 + n1, 1)
|
| 384 |
+
majority_share = max(n0, n1) / total
|
| 385 |
+
|
| 386 |
+
if mode == "off":
|
| 387 |
+
apply = False
|
| 388 |
+
reason = "class_weighting=off (forced by config)."
|
| 389 |
+
elif mode == "on":
|
| 390 |
+
apply = True
|
| 391 |
+
reason = "class_weighting=on (forced by config)."
|
| 392 |
+
else:
|
| 393 |
+
apply = majority_share > threshold
|
| 394 |
+
reason = (
|
| 395 |
+
f"Measured majority-class share {majority_share:.4f} "
|
| 396 |
+
f"{'exceeds' if apply else 'is within'} the {threshold} threshold, "
|
| 397 |
+
f"so weighted cross entropy is {'enabled' if apply else 'NOT used'}."
|
| 398 |
+
)
|
| 399 |
+
|
| 400 |
+
weights = None
|
| 401 |
+
if apply and n0 > 0 and n1 > 0:
|
| 402 |
+
# Inverse-frequency weights normalised to mean 1.
|
| 403 |
+
w = np.array([total / (2 * n0), total / (2 * n1)], dtype=np.float64)
|
| 404 |
+
weights = (w / w.mean()).tolist()
|
| 405 |
+
|
| 406 |
+
report = {
|
| 407 |
+
"mode": mode,
|
| 408 |
+
"threshold": threshold,
|
| 409 |
+
"majority_class_share": round(majority_share, 6),
|
| 410 |
+
"applied": weights is not None,
|
| 411 |
+
"weights": weights,
|
| 412 |
+
"reason": reason,
|
| 413 |
+
}
|
| 414 |
+
logger.info("Class weighting decision: %s", reason)
|
| 415 |
+
return weights, report
|
| 416 |
+
|
| 417 |
+
|
| 418 |
+
# --------------------------------------------------------------------------- #
|
| 419 |
+
# 4. GraphCodeBERT snippet features
|
| 420 |
+
# --------------------------------------------------------------------------- #
|
| 421 |
+
@dataclass
|
| 422 |
+
class SnippetFeatures:
|
| 423 |
+
"""Pre-tokenised snippet pool, laid out as flat numpy arrays.
|
| 424 |
+
|
| 425 |
+
``dfg_adj_*`` store the (ragged) node adjacency lists so that no data-flow
|
| 426 |
+
edge is ever clipped away silently.
|
| 427 |
+
"""
|
| 428 |
+
|
| 429 |
+
input_ids: np.ndarray # int32 [N, L]
|
| 430 |
+
position_idx: np.ndarray # int16 [N, L]
|
| 431 |
+
dfg_to_code: np.ndarray # int32 [N, max_nodes, 2] (offsets into the
|
| 432 |
+
#: *untruncated* sub-token stream, so they can exceed the sequence length)
|
| 433 |
+
num_nodes: np.ndarray # int16 [N]
|
| 434 |
+
node_index: np.ndarray # int16 [N] number of real code tokens (incl. <s>/</s>)
|
| 435 |
+
max_length: np.ndarray # int16 [N] code tokens + data-flow nodes
|
| 436 |
+
dfg_adj_values: np.ndarray # int16 [total_edges]
|
| 437 |
+
dfg_adj_offsets: np.ndarray # int64 [N, max_nodes + 1]
|
| 438 |
+
seq_length: int
|
| 439 |
+
stats: dict[str, Any]
|
| 440 |
+
|
| 441 |
+
def __len__(self) -> int:
|
| 442 |
+
return int(self.input_ids.shape[0])
|
| 443 |
+
|
| 444 |
+
|
| 445 |
+
def build_snippet_features(
|
| 446 |
+
cfg: Config, snippets: list[str], tokenizer: Any, num_proc: int | None = None
|
| 447 |
+
) -> SnippetFeatures:
|
| 448 |
+
"""Run data-flow extraction + tokenisation over every distinct snippet.
|
| 449 |
+
|
| 450 |
+
Uses ``datasets.map`` (batched, multi-process, Arrow-cached) so that a rerun
|
| 451 |
+
with the same config costs nothing.
|
| 452 |
+
"""
|
| 453 |
+
from datasets import Dataset as HFDataset
|
| 454 |
+
|
| 455 |
+
seq_len = cfg.total_sequence_length
|
| 456 |
+
max_nodes = seq_len - 3 # hard upper bound; the real cap is computed per snippet
|
| 457 |
+
cache = Path(cfg.cache_dir) / "features"
|
| 458 |
+
cache.mkdir(parents=True, exist_ok=True)
|
| 459 |
+
key = _fingerprint(
|
| 460 |
+
cfg.dataset_name, cfg.model_name_or_path, cfg.code_length, cfg.data_flow_length, len(snippets)
|
| 461 |
+
)
|
| 462 |
+
npz_path = cache / f"snippets_{key}.npz"
|
| 463 |
+
stats_path = cache / f"snippets_{key}.stats.json"
|
| 464 |
+
|
| 465 |
+
if npz_path.exists() and stats_path.exists():
|
| 466 |
+
logger.info("Reusing cached snippet features at %s", npz_path)
|
| 467 |
+
z = np.load(npz_path)
|
| 468 |
+
return SnippetFeatures(
|
| 469 |
+
input_ids=z["input_ids"],
|
| 470 |
+
position_idx=z["position_idx"],
|
| 471 |
+
dfg_to_code=z["dfg_to_code"],
|
| 472 |
+
num_nodes=z["num_nodes"],
|
| 473 |
+
node_index=z["node_index"],
|
| 474 |
+
max_length=z["max_length"],
|
| 475 |
+
dfg_adj_values=z["dfg_adj_values"],
|
| 476 |
+
dfg_adj_offsets=z["dfg_adj_offsets"],
|
| 477 |
+
seq_length=seq_len,
|
| 478 |
+
stats=json.loads(stats_path.read_text(encoding="utf-8")),
|
| 479 |
+
)
|
| 480 |
+
|
| 481 |
+
ds = HFDataset.from_dict({"code": snippets})
|
| 482 |
+
num_proc = num_proc if num_proc and num_proc > 1 else None
|
| 483 |
+
t0 = time.time()
|
| 484 |
+
ds = ds.map(
|
| 485 |
+
_make_feature_fn(tokenizer, cfg.code_length, cfg.data_flow_length),
|
| 486 |
+
batched=True,
|
| 487 |
+
batch_size=256,
|
| 488 |
+
num_proc=num_proc,
|
| 489 |
+
remove_columns=["code"],
|
| 490 |
+
desc="GraphCodeBERT data-flow + tokenisation",
|
| 491 |
+
)
|
| 492 |
+
|
| 493 |
+
n = len(ds)
|
| 494 |
+
cols = ds.with_format(None)
|
| 495 |
+
status_counter: Counter[str] = Counter()
|
| 496 |
+
n_nodes_all: list[int] = []
|
| 497 |
+
|
| 498 |
+
input_ids = np.full((n, seq_len), tokenizer.pad_token_id, dtype=np.int32)
|
| 499 |
+
position_idx = np.full((n, seq_len), tokenizer.pad_token_id, dtype=np.int16)
|
| 500 |
+
num_nodes = np.zeros(n, dtype=np.int16)
|
| 501 |
+
node_index = np.zeros(n, dtype=np.int16)
|
| 502 |
+
max_length = np.zeros(n, dtype=np.int16)
|
| 503 |
+
dfg_to_code_list: list[list[list[int]]] = []
|
| 504 |
+
adj_values: list[int] = []
|
| 505 |
+
adj_offsets = np.zeros((n, max_nodes + 1), dtype=np.int64)
|
| 506 |
+
|
| 507 |
+
observed_max_nodes = 0
|
| 508 |
+
for i, row in enumerate(cols):
|
| 509 |
+
ids = row["input_ids"]
|
| 510 |
+
pos = row["position_idx"]
|
| 511 |
+
input_ids[i, : len(ids)] = ids
|
| 512 |
+
position_idx[i, : len(pos)] = pos
|
| 513 |
+
d2c = row["dfg_to_code"]
|
| 514 |
+
adj = row["dfg_to_dfg"]
|
| 515 |
+
num_nodes[i] = len(d2c)
|
| 516 |
+
observed_max_nodes = max(observed_max_nodes, len(d2c))
|
| 517 |
+
node_index[i] = int(np.sum(np.asarray(pos) > 1))
|
| 518 |
+
max_length[i] = int(np.sum(np.asarray(pos) != tokenizer.pad_token_id))
|
| 519 |
+
dfg_to_code_list.append(d2c)
|
| 520 |
+
base = len(adj_values)
|
| 521 |
+
adj_offsets[i, 0] = base
|
| 522 |
+
for j, nb in enumerate(adj):
|
| 523 |
+
adj_values.extend(nb)
|
| 524 |
+
adj_offsets[i, j + 1] = len(adj_values)
|
| 525 |
+
adj_offsets[i, len(adj) + 1 :] = len(adj_values)
|
| 526 |
+
status_counter.update(row["status_flags"])
|
| 527 |
+
n_nodes_all.append(len(d2c))
|
| 528 |
+
|
| 529 |
+
observed_max_nodes = max(observed_max_nodes, 1)
|
| 530 |
+
# int32: these are offsets into the untruncated sub-token stream, which for a
|
| 531 |
+
# pathologically long snippet reaches ~1e5 -- well past int16.
|
| 532 |
+
dfg_to_code = np.zeros((n, observed_max_nodes, 2), dtype=np.int32)
|
| 533 |
+
for i, d2c in enumerate(dfg_to_code_list):
|
| 534 |
+
if d2c:
|
| 535 |
+
dfg_to_code[i, : len(d2c)] = np.asarray(d2c, dtype=np.int32)
|
| 536 |
+
adj_offsets = adj_offsets[:, : observed_max_nodes + 1]
|
| 537 |
+
|
| 538 |
+
nodes_arr = np.asarray(n_nodes_all)
|
| 539 |
+
stats = {
|
| 540 |
+
"num_snippets": n,
|
| 541 |
+
"extraction_seconds": round(time.time() - t0, 1),
|
| 542 |
+
"status_counts": dict(status_counter),
|
| 543 |
+
"snippets_with_empty_dataflow": int((nodes_arr == 0).sum()),
|
| 544 |
+
"dataflow_nodes_mean": round(float(nodes_arr.mean()), 2),
|
| 545 |
+
"dataflow_nodes_p50": int(np.percentile(nodes_arr, 50)),
|
| 546 |
+
"dataflow_nodes_p95": int(np.percentile(nodes_arr, 95)),
|
| 547 |
+
"dataflow_nodes_max": int(nodes_arr.max()),
|
| 548 |
+
"code_tokens_mean": round(float(node_index.mean()), 2),
|
| 549 |
+
"code_tokens_truncated": int((node_index >= cfg.code_length - 1).sum()),
|
| 550 |
+
"total_dataflow_edges": len(adj_values),
|
| 551 |
+
"sequence_length": seq_len,
|
| 552 |
+
}
|
| 553 |
+
logger.info("Snippet features: %s", json.dumps(stats, indent=2))
|
| 554 |
+
if status_counter.get("dfg_failed", 0) or status_counter.get("dfg_recursion_limit", 0):
|
| 555 |
+
logger.warning(
|
| 556 |
+
"Data-flow extraction degraded for %d snippets (kept with an empty graph, not dropped).",
|
| 557 |
+
status_counter.get("dfg_failed", 0) + status_counter.get("dfg_recursion_limit", 0),
|
| 558 |
+
)
|
| 559 |
+
|
| 560 |
+
features = SnippetFeatures(
|
| 561 |
+
input_ids=input_ids,
|
| 562 |
+
position_idx=position_idx,
|
| 563 |
+
dfg_to_code=dfg_to_code,
|
| 564 |
+
num_nodes=num_nodes,
|
| 565 |
+
node_index=node_index,
|
| 566 |
+
max_length=max_length,
|
| 567 |
+
dfg_adj_values=np.asarray(adj_values, dtype=np.int16),
|
| 568 |
+
dfg_adj_offsets=adj_offsets,
|
| 569 |
+
seq_length=seq_len,
|
| 570 |
+
stats=stats,
|
| 571 |
+
)
|
| 572 |
+
np.savez_compressed(
|
| 573 |
+
npz_path,
|
| 574 |
+
input_ids=features.input_ids,
|
| 575 |
+
position_idx=features.position_idx,
|
| 576 |
+
dfg_to_code=features.dfg_to_code,
|
| 577 |
+
num_nodes=features.num_nodes,
|
| 578 |
+
node_index=features.node_index,
|
| 579 |
+
max_length=features.max_length,
|
| 580 |
+
dfg_adj_values=features.dfg_adj_values,
|
| 581 |
+
dfg_adj_offsets=features.dfg_adj_offsets,
|
| 582 |
+
)
|
| 583 |
+
stats_path.write_text(json.dumps(stats, indent=2), encoding="utf-8")
|
| 584 |
+
return features
|
| 585 |
+
|
| 586 |
+
|
| 587 |
+
def _make_feature_fn(tokenizer: Any, code_length: int, data_flow_length: int):
|
| 588 |
+
"""Build the batched ``datasets.map`` function (picklable via closure)."""
|
| 589 |
+
seq_len = code_length + data_flow_length
|
| 590 |
+
cls_id, sep_id = tokenizer.cls_token_id, tokenizer.sep_token_id
|
| 591 |
+
pad_id, unk_id = tokenizer.pad_token_id, tokenizer.unk_token_id
|
| 592 |
+
|
| 593 |
+
def fn(batch: dict[str, list]) -> dict[str, list]:
|
| 594 |
+
out_ids, out_pos, out_d2c, out_d2d, out_status = [], [], [], [], []
|
| 595 |
+
for code in batch["code"]:
|
| 596 |
+
flags: list[str] = []
|
| 597 |
+
try:
|
| 598 |
+
code_tokens, dfg, status = extract_dataflow(code, DATASET_LANGUAGE)
|
| 599 |
+
except DataFlowExtractionError as exc:
|
| 600 |
+
# Never drop the example: fall back to a plain tokenisation with
|
| 601 |
+
# no data-flow component, and record the reason.
|
| 602 |
+
logger.warning("Data-flow extraction failed, using empty graph: %s", exc)
|
| 603 |
+
code_tokens, dfg = code.split(), []
|
| 604 |
+
status = {"comment_strip": "n/a", "parse": "failed", "dfg": "failed", "error": str(exc)}
|
| 605 |
+
for stage in ("comment_strip", "parse", "dfg"):
|
| 606 |
+
if status[stage] not in ("ok", "n/a"):
|
| 607 |
+
flags.append(f"{stage}_{status[stage]}")
|
| 608 |
+
if not flags:
|
| 609 |
+
flags.append("ok")
|
| 610 |
+
|
| 611 |
+
# GraphCodeBERT tokenises each code token separately; the '@ ' prefix
|
| 612 |
+
# trick forces a word-boundary BPE split for non-initial tokens.
|
| 613 |
+
sub_tokens = [
|
| 614 |
+
tokenizer.tokenize("@ " + t)[1:] if i != 0 else tokenizer.tokenize(t)
|
| 615 |
+
for i, t in enumerate(code_tokens)
|
| 616 |
+
]
|
| 617 |
+
ori2cur = {-1: (0, 0)}
|
| 618 |
+
for i in range(len(sub_tokens)):
|
| 619 |
+
prev_end = ori2cur[i - 1][1]
|
| 620 |
+
ori2cur[i] = (prev_end, prev_end + len(sub_tokens[i]))
|
| 621 |
+
flat = [y for x in sub_tokens for y in x]
|
| 622 |
+
|
| 623 |
+
# Reserve room for the data-flow nodes, then for <s>/</s>.
|
| 624 |
+
keep = seq_len - 3 - min(len(dfg), data_flow_length)
|
| 625 |
+
flat = flat[:keep][: code_length - 3]
|
| 626 |
+
|
| 627 |
+
source_tokens = [tokenizer.cls_token] + flat + [tokenizer.sep_token]
|
| 628 |
+
source_ids = tokenizer.convert_tokens_to_ids(source_tokens)
|
| 629 |
+
# Code tokens get positions 2..; data-flow nodes get 0; padding gets
|
| 630 |
+
# pad_token_id (1). This is what the model uses to tell them apart.
|
| 631 |
+
position_idx = [i + pad_id + 1 for i in range(len(source_tokens))]
|
| 632 |
+
|
| 633 |
+
dfg = dfg[: seq_len - len(source_tokens)]
|
| 634 |
+
source_ids += [unk_id] * len(dfg)
|
| 635 |
+
position_idx += [0] * len(dfg)
|
| 636 |
+
padding = seq_len - len(source_ids)
|
| 637 |
+
source_ids += [pad_id] * padding
|
| 638 |
+
position_idx += [pad_id] * padding
|
| 639 |
+
|
| 640 |
+
# Re-index edges so they point at node slots, not original token ids.
|
| 641 |
+
reverse = {x[1]: i for i, x in enumerate(dfg)}
|
| 642 |
+
dfg_to_dfg = [[reverse[i] for i in x[-1] if i in reverse] for x in dfg]
|
| 643 |
+
dfg_to_code = [[ori2cur[x[1]][0] + 1, ori2cur[x[1]][1] + 1] for x in dfg]
|
| 644 |
+
|
| 645 |
+
out_ids.append(source_ids)
|
| 646 |
+
out_pos.append(position_idx)
|
| 647 |
+
out_d2c.append(dfg_to_code)
|
| 648 |
+
out_d2d.append(dfg_to_dfg)
|
| 649 |
+
out_status.append(flags)
|
| 650 |
+
|
| 651 |
+
return {
|
| 652 |
+
"input_ids": out_ids,
|
| 653 |
+
"position_idx": out_pos,
|
| 654 |
+
"dfg_to_code": out_d2c,
|
| 655 |
+
"dfg_to_dfg": out_d2d,
|
| 656 |
+
"status_flags": out_status,
|
| 657 |
+
}
|
| 658 |
+
|
| 659 |
+
return fn
|
| 660 |
+
|
| 661 |
+
|
| 662 |
+
# --------------------------------------------------------------------------- #
|
| 663 |
+
# 5. Torch dataset + graph-guided attention-mask collator
|
| 664 |
+
# --------------------------------------------------------------------------- #
|
| 665 |
+
class ClonePairDataset(Dataset):
|
| 666 |
+
"""Pairs as ``(snippet_id1, snippet_id2, label)`` over a shared feature pool."""
|
| 667 |
+
|
| 668 |
+
def __init__(self, index: SplitIndex, features: SnippetFeatures) -> None:
|
| 669 |
+
self.index = index
|
| 670 |
+
self.features = features
|
| 671 |
+
|
| 672 |
+
def __len__(self) -> int:
|
| 673 |
+
return len(self.index)
|
| 674 |
+
|
| 675 |
+
def __getitem__(self, i: int) -> tuple[int, int, int]:
|
| 676 |
+
return (
|
| 677 |
+
int(self.index.snippet_id1[i]),
|
| 678 |
+
int(self.index.snippet_id2[i]),
|
| 679 |
+
int(self.index.labels[i]),
|
| 680 |
+
)
|
| 681 |
+
|
| 682 |
+
|
| 683 |
+
def build_graph_attention_mask(features: SnippetFeatures, sid: int) -> np.ndarray:
|
| 684 |
+
"""Construct GraphCodeBERT's graph-guided masked attention for one snippet.
|
| 685 |
+
|
| 686 |
+
Four rules, exactly as in the paper:
|
| 687 |
+
|
| 688 |
+
1. code tokens attend to code tokens;
|
| 689 |
+
2. the special tokens ``<s>``/``</s>`` attend to everything real;
|
| 690 |
+
3. a data-flow node attends to (and is attended by) the code tokens it was
|
| 691 |
+
identified from;
|
| 692 |
+
4. a data-flow node attends to its adjacent nodes in the graph.
|
| 693 |
+
"""
|
| 694 |
+
L = features.seq_length
|
| 695 |
+
mask = np.zeros((L, L), dtype=bool)
|
| 696 |
+
node_index = int(features.node_index[sid])
|
| 697 |
+
max_length = int(features.max_length[sid])
|
| 698 |
+
n_nodes = int(features.num_nodes[sid])
|
| 699 |
+
|
| 700 |
+
# (1) sequence attends to sequence
|
| 701 |
+
mask[:node_index, :node_index] = True
|
| 702 |
+
# (2) special tokens attend to all real positions
|
| 703 |
+
ids = features.input_ids[sid]
|
| 704 |
+
for pos in np.flatnonzero((ids == 0) | (ids == 2)):
|
| 705 |
+
if pos < node_index:
|
| 706 |
+
mask[pos, :max_length] = True
|
| 707 |
+
# (3) nodes <-> the code tokens they come from
|
| 708 |
+
d2c = features.dfg_to_code[sid]
|
| 709 |
+
for j in range(n_nodes):
|
| 710 |
+
a, b = int(d2c[j, 0]), int(d2c[j, 1])
|
| 711 |
+
if a < node_index and b < node_index:
|
| 712 |
+
mask[j + node_index, a:b] = True
|
| 713 |
+
mask[a:b, j + node_index] = True
|
| 714 |
+
# (4) nodes <-> adjacent nodes
|
| 715 |
+
offsets = features.dfg_adj_offsets[sid]
|
| 716 |
+
for j in range(n_nodes):
|
| 717 |
+
nbrs = features.dfg_adj_values[offsets[j] : offsets[j + 1]]
|
| 718 |
+
for a in nbrs:
|
| 719 |
+
if int(a) + node_index < L:
|
| 720 |
+
mask[j + node_index, int(a) + node_index] = True
|
| 721 |
+
return mask
|
| 722 |
+
|
| 723 |
+
|
| 724 |
+
@dataclass
|
| 725 |
+
class CloneCollator:
|
| 726 |
+
"""Collate pairs into the tensors ``GraphCodeBERTForCloneDetection`` expects."""
|
| 727 |
+
|
| 728 |
+
features: SnippetFeatures
|
| 729 |
+
|
| 730 |
+
def __call__(self, batch: list[tuple[int, int, int]]) -> dict[str, torch.Tensor]:
|
| 731 |
+
ids1 = [b[0] for b in batch]
|
| 732 |
+
ids2 = [b[1] for b in batch]
|
| 733 |
+
labels = [b[2] for b in batch]
|
| 734 |
+
f = self.features
|
| 735 |
+
return {
|
| 736 |
+
"input_ids_1": torch.from_numpy(f.input_ids[ids1].astype(np.int64)),
|
| 737 |
+
"position_idx_1": torch.from_numpy(f.position_idx[ids1].astype(np.int64)),
|
| 738 |
+
"attn_mask_1": torch.from_numpy(
|
| 739 |
+
np.stack([build_graph_attention_mask(f, i) for i in ids1])
|
| 740 |
+
),
|
| 741 |
+
"input_ids_2": torch.from_numpy(f.input_ids[ids2].astype(np.int64)),
|
| 742 |
+
"position_idx_2": torch.from_numpy(f.position_idx[ids2].astype(np.int64)),
|
| 743 |
+
"attn_mask_2": torch.from_numpy(
|
| 744 |
+
np.stack([build_graph_attention_mask(f, i) for i in ids2])
|
| 745 |
+
),
|
| 746 |
+
"labels": torch.tensor(labels, dtype=torch.long),
|
| 747 |
+
}
|
| 748 |
+
|
| 749 |
+
|
| 750 |
+
# --------------------------------------------------------------------------- #
|
| 751 |
+
# 6. One-call pipeline used by train.py / evaluate.py
|
| 752 |
+
# --------------------------------------------------------------------------- #
|
| 753 |
+
@dataclass
|
| 754 |
+
class PreparedData:
|
| 755 |
+
train: ClonePairDataset | None
|
| 756 |
+
validation: ClonePairDataset
|
| 757 |
+
test: ClonePairDataset
|
| 758 |
+
features: SnippetFeatures
|
| 759 |
+
collator: CloneCollator
|
| 760 |
+
class_weights: list[float] | None
|
| 761 |
+
report: dict[str, Any]
|
| 762 |
+
|
| 763 |
+
|
| 764 |
+
def prepare_data(cfg: Config, tokenizer: Any, with_train: bool = True) -> PreparedData:
|
| 765 |
+
"""Load, split, verify and featurise the dataset end to end."""
|
| 766 |
+
schema = verify_dataset_schema(cfg.dataset_name)
|
| 767 |
+
snippets, indices, pool_stats = build_snippet_pool(
|
| 768 |
+
cfg, (cfg.train_split, cfg.heldout_split)
|
| 769 |
+
)
|
| 770 |
+
|
| 771 |
+
train_idx = indices[cfg.train_split]
|
| 772 |
+
validation_idx, test_idx, split_report = split_heldout_by_group(
|
| 773 |
+
indices[cfg.heldout_split], cfg.test_group_fraction, cfg.seed
|
| 774 |
+
)
|
| 775 |
+
|
| 776 |
+
train_idx, train_sub = subsample(
|
| 777 |
+
train_idx, cfg.max_train_samples, cfg.seed, cfg.balance_subsamples
|
| 778 |
+
)
|
| 779 |
+
validation_idx, val_sub = subsample(
|
| 780 |
+
validation_idx, cfg.max_eval_samples, cfg.seed, cfg.balance_subsamples
|
| 781 |
+
)
|
| 782 |
+
test_idx, test_sub = subsample(
|
| 783 |
+
test_idx, cfg.max_test_samples, cfg.seed, cfg.balance_subsamples
|
| 784 |
+
)
|
| 785 |
+
|
| 786 |
+
_assert_no_leakage({"train": train_idx, "validation": validation_idx, "test": test_idx})
|
| 787 |
+
|
| 788 |
+
class_weights, weight_report = decide_class_weights(
|
| 789 |
+
train_idx, cfg.class_weighting, cfg.class_weight_threshold
|
| 790 |
+
)
|
| 791 |
+
|
| 792 |
+
features = build_snippet_features(cfg, snippets, tokenizer, cfg.preprocessing_num_workers)
|
| 793 |
+
collator = CloneCollator(features)
|
| 794 |
+
|
| 795 |
+
report = {
|
| 796 |
+
"schema": schema,
|
| 797 |
+
"snippet_pool": pool_stats,
|
| 798 |
+
"heldout_split_strategy": split_report,
|
| 799 |
+
"subsampling": {"train": train_sub, "validation": val_sub, "test": test_sub},
|
| 800 |
+
"class_distribution": {
|
| 801 |
+
"train": train_idx.class_distribution(),
|
| 802 |
+
"validation": validation_idx.class_distribution(),
|
| 803 |
+
"test": test_idx.class_distribution(),
|
| 804 |
+
},
|
| 805 |
+
"class_weighting": weight_report,
|
| 806 |
+
"feature_extraction": features.stats,
|
| 807 |
+
}
|
| 808 |
+
|
| 809 |
+
return PreparedData(
|
| 810 |
+
train=ClonePairDataset(train_idx, features) if with_train else None,
|
| 811 |
+
validation=ClonePairDataset(validation_idx, features),
|
| 812 |
+
test=ClonePairDataset(test_idx, features),
|
| 813 |
+
features=features,
|
| 814 |
+
collator=collator,
|
| 815 |
+
class_weights=class_weights,
|
| 816 |
+
report=report,
|
| 817 |
+
)
|
| 818 |
+
|
| 819 |
+
|
| 820 |
+
def _assert_no_leakage(splits: dict[str, SplitIndex]) -> None:
|
| 821 |
+
"""Hard gate: no snippet and no group may appear in two splits."""
|
| 822 |
+
names = list(splits)
|
| 823 |
+
for i, a in enumerate(names):
|
| 824 |
+
sa = set(splits[a].snippet_ids().tolist())
|
| 825 |
+
ga = set(np.unique(np.concatenate([splits[a].group1, splits[a].group2])).tolist())
|
| 826 |
+
for b in names[i + 1 :]:
|
| 827 |
+
sb = set(splits[b].snippet_ids().tolist())
|
| 828 |
+
gb = set(np.unique(np.concatenate([splits[b].group1, splits[b].group2])).tolist())
|
| 829 |
+
if sa & sb:
|
| 830 |
+
raise AssertionError(f"LEAK: {len(sa & sb)} snippets shared by {a} and {b}.")
|
| 831 |
+
if ga & gb:
|
| 832 |
+
raise AssertionError(f"LEAK: {len(ga & gb)} groups shared by {a} and {b}.")
|
| 833 |
+
logger.info("Leakage check passed: splits share no snippet and no problem group.")
|
| 834 |
+
|
| 835 |
+
|
| 836 |
+
def iter_batches(dataset: Dataset, collator: CloneCollator, batch_size: int) -> Iterator[dict]:
|
| 837 |
+
"""Small helper for scripts that need batches without a Trainer."""
|
| 838 |
+
for start in range(0, len(dataset), batch_size):
|
| 839 |
+
yield collator([dataset[i] for i in range(start, min(start + batch_size, len(dataset)))])
|
code/requirements.txt
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Core
|
| 2 |
+
torch>=2.4
|
| 3 |
+
transformers>=5.0
|
| 4 |
+
datasets>=3.0
|
| 5 |
+
accelerate>=1.0
|
| 6 |
+
tokenizers>=0.20
|
| 7 |
+
|
| 8 |
+
# GraphCodeBERT data-flow extraction (tree-sitter is required by the original
|
| 9 |
+
# GraphCodeBERT preprocessing pipeline -- it is not an optional extra).
|
| 10 |
+
tree-sitter>=0.21
|
| 11 |
+
tree-sitter-python>=0.21
|
| 12 |
+
|
| 13 |
+
# Data / metrics
|
| 14 |
+
numpy>=1.24
|
| 15 |
+
pyarrow>=14
|
| 16 |
+
scikit-learn>=1.3
|
| 17 |
+
huggingface-hub>=0.26
|
config.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"add_cross_attention": false,
|
| 3 |
+
"architectures": [
|
| 4 |
+
"GraphCodeBERTForCloneDetection"
|
| 5 |
+
],
|
| 6 |
+
"attention_probs_dropout_prob": 0.1,
|
| 7 |
+
"bos_token_id": 0,
|
| 8 |
+
"classifier_dropout": null,
|
| 9 |
+
"dtype": "float32",
|
| 10 |
+
"eos_token_id": 2,
|
| 11 |
+
"gradient_checkpointing": false,
|
| 12 |
+
"hidden_act": "gelu",
|
| 13 |
+
"hidden_dropout_prob": 0.1,
|
| 14 |
+
"hidden_size": 768,
|
| 15 |
+
"initializer_range": 0.02,
|
| 16 |
+
"intermediate_size": 3072,
|
| 17 |
+
"is_decoder": false,
|
| 18 |
+
"layer_norm_eps": 1e-05,
|
| 19 |
+
"max_position_embeddings": 514,
|
| 20 |
+
"model_type": "roberta",
|
| 21 |
+
"num_attention_heads": 12,
|
| 22 |
+
"num_hidden_layers": 12,
|
| 23 |
+
"output_past": true,
|
| 24 |
+
"pad_token_id": 1,
|
| 25 |
+
"tie_word_embeddings": true,
|
| 26 |
+
"transformers_version": "5.17.0",
|
| 27 |
+
"type_vocab_size": 1,
|
| 28 |
+
"use_cache": false,
|
| 29 |
+
"vocab_size": 50265
|
| 30 |
+
}
|
experiment_record.json
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_name": "microsoft/graphcodebert-base",
|
| 3 |
+
"model_dir": "/mnt/d/work2/graphcodebert-code-clone-detection/models/graphcodebert-clone-detection",
|
| 4 |
+
"dataset_name": "PoolC/1-fold-clone-detection-600k-5fold",
|
| 5 |
+
"dataset_split_strategy": {
|
| 6 |
+
"train_split": "train",
|
| 7 |
+
"heldout_split": "val",
|
| 8 |
+
"note": "The repository provides one of 5 predefined folds as `train` + `val`; those groups are disjoint and are kept as-is. `val` is partitioned further into validation/test along problem-group boundaries.",
|
| 9 |
+
"heldout_groups": 59,
|
| 10 |
+
"validation_groups": 29,
|
| 11 |
+
"test_groups": 30,
|
| 12 |
+
"dropped_cross_boundary_pairs": 337398,
|
| 13 |
+
"validation": {
|
| 14 |
+
"num_examples": 483738,
|
| 15 |
+
"negatives_label_0": 157558,
|
| 16 |
+
"positives_label_1": 326180,
|
| 17 |
+
"positive_ratio": 0.674291,
|
| 18 |
+
"num_groups": 29
|
| 19 |
+
},
|
| 20 |
+
"test": {
|
| 21 |
+
"num_examples": 503224,
|
| 22 |
+
"negatives_label_0": 167224,
|
| 23 |
+
"positives_label_1": 336000,
|
| 24 |
+
"positive_ratio": 0.667695,
|
| 25 |
+
"num_groups": 30
|
| 26 |
+
}
|
| 27 |
+
},
|
| 28 |
+
"num_train_examples": 50000,
|
| 29 |
+
"num_validation_examples": 20000,
|
| 30 |
+
"num_test_examples": 20000,
|
| 31 |
+
"class_distributions": {
|
| 32 |
+
"train": {
|
| 33 |
+
"num_examples": 50000,
|
| 34 |
+
"negatives_label_0": 25000,
|
| 35 |
+
"positives_label_1": 25000,
|
| 36 |
+
"positive_ratio": 0.5,
|
| 37 |
+
"num_groups": 240
|
| 38 |
+
},
|
| 39 |
+
"validation": {
|
| 40 |
+
"num_examples": 20000,
|
| 41 |
+
"negatives_label_0": 10000,
|
| 42 |
+
"positives_label_1": 10000,
|
| 43 |
+
"positive_ratio": 0.5,
|
| 44 |
+
"num_groups": 29
|
| 45 |
+
},
|
| 46 |
+
"test": {
|
| 47 |
+
"num_examples": 20000,
|
| 48 |
+
"negatives_label_0": 10000,
|
| 49 |
+
"positives_label_1": 10000,
|
| 50 |
+
"positive_ratio": 0.5,
|
| 51 |
+
"num_groups": 30
|
| 52 |
+
}
|
| 53 |
+
},
|
| 54 |
+
"class_weighting": {
|
| 55 |
+
"mode": "auto",
|
| 56 |
+
"threshold": 0.6,
|
| 57 |
+
"majority_class_share": 0.5,
|
| 58 |
+
"applied": false,
|
| 59 |
+
"weights": null,
|
| 60 |
+
"reason": "Measured majority-class share 0.5000 is within the 0.6 threshold, so weighted cross entropy is NOT used."
|
| 61 |
+
},
|
| 62 |
+
"sequence_length": 512,
|
| 63 |
+
"data_flow_length": 128,
|
| 64 |
+
"total_sequence_length": 640,
|
| 65 |
+
"per_device_train_batch_size": 16,
|
| 66 |
+
"gradient_accumulation_steps": 1,
|
| 67 |
+
"effective_batch_size": 16,
|
| 68 |
+
"learning_rate": 2e-05,
|
| 69 |
+
"num_train_epochs": 3.0,
|
| 70 |
+
"optimizer": "adamw_torch",
|
| 71 |
+
"scheduler": "linear",
|
| 72 |
+
"warmup_ratio": 0.1,
|
| 73 |
+
"warmup_steps": 938,
|
| 74 |
+
"weight_decay": 0.01,
|
| 75 |
+
"max_grad_norm": 1.0,
|
| 76 |
+
"mixed_precision": "fp16",
|
| 77 |
+
"gradient_checkpointing": false,
|
| 78 |
+
"seed": 42,
|
| 79 |
+
"training_time_seconds": 6921.9,
|
| 80 |
+
"training_time_hours": 1.923,
|
| 81 |
+
"train_runtime_metrics": {
|
| 82 |
+
"train_runtime": 6918.8074,
|
| 83 |
+
"train_samples_per_second": 21.68,
|
| 84 |
+
"train_steps_per_second": 1.355,
|
| 85 |
+
"total_flos": 0.0,
|
| 86 |
+
"train_loss": 0.43145505716959637,
|
| 87 |
+
"epoch": 3.0
|
| 88 |
+
},
|
| 89 |
+
"gpu": {
|
| 90 |
+
"cuda_available": true,
|
| 91 |
+
"torch_version": "2.11.0+cu128",
|
| 92 |
+
"transformers_version": "5.17.0",
|
| 93 |
+
"python_version": "3.12.3",
|
| 94 |
+
"platform": "Linux-6.18.33.2-microsoft-standard-WSL2-x86_64-with-glibc2.39",
|
| 95 |
+
"gpu_name": "NVIDIA GeForce RTX 5060 Ti",
|
| 96 |
+
"gpu_count": 1,
|
| 97 |
+
"gpu_total_memory_gb": 15.9,
|
| 98 |
+
"gpu_capability": "12.0",
|
| 99 |
+
"cuda_version": "12.8"
|
| 100 |
+
},
|
| 101 |
+
"parameters": {
|
| 102 |
+
"trainable_parameters": 125236994,
|
| 103 |
+
"total_parameters": 125236994
|
| 104 |
+
},
|
| 105 |
+
"best_validation_f1": 0.8671882190520018,
|
| 106 |
+
"best_checkpoint": "./outputs/checkpoint-9000",
|
| 107 |
+
"validation_metrics": {
|
| 108 |
+
"loss": 0.34459105134010315,
|
| 109 |
+
"accuracy": 0.8557,
|
| 110 |
+
"precision": 0.8032395566922421,
|
| 111 |
+
"recall": 0.9422,
|
| 112 |
+
"f1": 0.8671882190520018,
|
| 113 |
+
"macro_f1": 0.8546121719233737,
|
| 114 |
+
"tp": 9422,
|
| 115 |
+
"tn": 7692,
|
| 116 |
+
"fp": 2308,
|
| 117 |
+
"fn": 578,
|
| 118 |
+
"confusion_matrix": [
|
| 119 |
+
[
|
| 120 |
+
7692,
|
| 121 |
+
2308
|
| 122 |
+
],
|
| 123 |
+
[
|
| 124 |
+
578,
|
| 125 |
+
9422
|
| 126 |
+
]
|
| 127 |
+
],
|
| 128 |
+
"confusion_matrix_layout": "[[TN, FP], [FN, TP]]",
|
| 129 |
+
"support": {
|
| 130 |
+
"num_examples": 20000,
|
| 131 |
+
"label_0": 10000,
|
| 132 |
+
"label_1": 10000
|
| 133 |
+
},
|
| 134 |
+
"runtime": 173.9737,
|
| 135 |
+
"samples_per_second": 114.96,
|
| 136 |
+
"steps_per_second": 3.592,
|
| 137 |
+
"num_examples": 20000,
|
| 138 |
+
"eval_seconds": 174.0
|
| 139 |
+
},
|
| 140 |
+
"test_metrics": {
|
| 141 |
+
"loss": 0.3165612816810608,
|
| 142 |
+
"model_preparation_time": 0.0022,
|
| 143 |
+
"accuracy": 0.87465,
|
| 144 |
+
"precision": 0.8409939018840448,
|
| 145 |
+
"recall": 0.924,
|
| 146 |
+
"f1": 0.8805450993472149,
|
| 147 |
+
"macro_f1": 0.874343974488208,
|
| 148 |
+
"tp": 9240,
|
| 149 |
+
"tn": 8253,
|
| 150 |
+
"fp": 1747,
|
| 151 |
+
"fn": 760,
|
| 152 |
+
"confusion_matrix": [
|
| 153 |
+
[
|
| 154 |
+
8253,
|
| 155 |
+
1747
|
| 156 |
+
],
|
| 157 |
+
[
|
| 158 |
+
760,
|
| 159 |
+
9240
|
| 160 |
+
]
|
| 161 |
+
],
|
| 162 |
+
"confusion_matrix_layout": "[[TN, FP], [FN, TP]]",
|
| 163 |
+
"support": {
|
| 164 |
+
"num_examples": 20000,
|
| 165 |
+
"label_0": 10000,
|
| 166 |
+
"label_1": 10000
|
| 167 |
+
},
|
| 168 |
+
"runtime": 181.6455,
|
| 169 |
+
"samples_per_second": 110.105,
|
| 170 |
+
"steps_per_second": 3.441,
|
| 171 |
+
"num_examples": 20000,
|
| 172 |
+
"eval_seconds": 181.7
|
| 173 |
+
},
|
| 174 |
+
"preprocessing": {
|
| 175 |
+
"snippet_pool": {
|
| 176 |
+
"num_unique_snippets": 44950,
|
| 177 |
+
"scan_seconds": 46.3,
|
| 178 |
+
"snippets_with_multiple_groups": 1,
|
| 179 |
+
"cross_split_snippet_overlap": {
|
| 180 |
+
"train|val:snippets": 0,
|
| 181 |
+
"train|val:groups": 0
|
| 182 |
+
},
|
| 183 |
+
"per_split": {
|
| 184 |
+
"train": {
|
| 185 |
+
"num_examples": 5388622,
|
| 186 |
+
"negatives_label_0": 2694311,
|
| 187 |
+
"positives_label_1": 2694311,
|
| 188 |
+
"positive_ratio": 0.5,
|
| 189 |
+
"num_groups": 240
|
| 190 |
+
},
|
| 191 |
+
"val": {
|
| 192 |
+
"num_examples": 1324360,
|
| 193 |
+
"negatives_label_0": 662180,
|
| 194 |
+
"positives_label_1": 662180,
|
| 195 |
+
"positive_ratio": 0.5,
|
| 196 |
+
"num_groups": 59
|
| 197 |
+
}
|
| 198 |
+
}
|
| 199 |
+
},
|
| 200 |
+
"feature_extraction": {
|
| 201 |
+
"num_snippets": 44950,
|
| 202 |
+
"extraction_seconds": 64.1,
|
| 203 |
+
"status_counts": {
|
| 204 |
+
"ok": 44930,
|
| 205 |
+
"comment_strip_failed": 13,
|
| 206 |
+
"dfg_failed": 7
|
| 207 |
+
},
|
| 208 |
+
"snippets_with_empty_dataflow": 263,
|
| 209 |
+
"dataflow_nodes_mean": 44.22,
|
| 210 |
+
"dataflow_nodes_p50": 33,
|
| 211 |
+
"dataflow_nodes_p95": 127,
|
| 212 |
+
"dataflow_nodes_max": 193,
|
| 213 |
+
"code_tokens_mean": 137.9,
|
| 214 |
+
"code_tokens_truncated": 885,
|
| 215 |
+
"total_dataflow_edges": 2481388,
|
| 216 |
+
"sequence_length": 640
|
| 217 |
+
},
|
| 218 |
+
"subsampling": {
|
| 219 |
+
"train": {
|
| 220 |
+
"subsampled": true,
|
| 221 |
+
"kept": 50000,
|
| 222 |
+
"balanced": true
|
| 223 |
+
},
|
| 224 |
+
"validation": {
|
| 225 |
+
"subsampled": true,
|
| 226 |
+
"kept": 20000,
|
| 227 |
+
"balanced": true
|
| 228 |
+
},
|
| 229 |
+
"test": {
|
| 230 |
+
"subsampled": true,
|
| 231 |
+
"kept": 20000,
|
| 232 |
+
"balanced": true
|
| 233 |
+
}
|
| 234 |
+
}
|
| 235 |
+
},
|
| 236 |
+
"sanity_check": {
|
| 237 |
+
"passed": true,
|
| 238 |
+
"device": "cuda",
|
| 239 |
+
"checks": [
|
| 240 |
+
{
|
| 241 |
+
"check": "1_dataset_loading",
|
| 242 |
+
"passed": true,
|
| 243 |
+
"detail": "train=50000 val=20000 test=20000"
|
| 244 |
+
},
|
| 245 |
+
{
|
| 246 |
+
"check": "2_column_detection",
|
| 247 |
+
"passed": true,
|
| 248 |
+
"detail": "code columns=code1/code2, label=similar; excluded from features: ['code1_group', 'code2_group', 'pair_id', 'question_pair_id']"
|
| 249 |
+
},
|
| 250 |
+
{
|
| 251 |
+
"check": "3_label_correctness",
|
| 252 |
+
"passed": true,
|
| 253 |
+
"detail": "labels in {0,1}; validation positive ratio=0.5"
|
| 254 |
+
},
|
| 255 |
+
{
|
| 256 |
+
"check": "4_tokenisation",
|
| 257 |
+
"passed": true,
|
| 258 |
+
"detail": "<s>...</s> wrapping OK, 93 code tokens, decodes to 'import math a , b , c = map ( int , input ( ) . split ( ) ) '"
|
| 259 |
+
},
|
| 260 |
+
{
|
| 261 |
+
"check": "5_dataflow_extraction",
|
| 262 |
+
"passed": true,
|
| 263 |
+
"detail": "44687/44950 snippets have a non-empty data-flow graph, 2481388 edges, mean nodes=44.22"
|
| 264 |
+
},
|
| 265 |
+
{
|
| 266 |
+
"check": "6_attention_mask",
|
| 267 |
+
"passed": true,
|
| 268 |
+
"detail": "shape=(640, 640) (code_length 512 + data_flow_length 128), 93 code tokens, 30 nodes, density=0.0215, padding rows empty"
|
| 269 |
+
},
|
| 270 |
+
{
|
| 271 |
+
"check": "7_forward_pass",
|
| 272 |
+
"passed": true,
|
| 273 |
+
"detail": "logits shape=(16, 2) for batch of 16 pairs; mask tensor=(16, 640, 640)"
|
| 274 |
+
},
|
| 275 |
+
{
|
| 276 |
+
"check": "8_finite_loss",
|
| 277 |
+
"passed": true,
|
| 278 |
+
"detail": "loss=0.6978 (chance level ~0.6931)"
|
| 279 |
+
},
|
| 280 |
+
{
|
| 281 |
+
"check": "9_training_step",
|
| 282 |
+
"passed": true,
|
| 283 |
+
"detail": "loss=0.6798, grad_norm=2.4929, weights updated (amp=on, dtype=torch.float16)"
|
| 284 |
+
}
|
| 285 |
+
],
|
| 286 |
+
"parameters": {
|
| 287 |
+
"trainable_parameters": 125236994,
|
| 288 |
+
"total_parameters": 125236994
|
| 289 |
+
},
|
| 290 |
+
"sequence_length": 640
|
| 291 |
+
},
|
| 292 |
+
"test_metrics_provenance": {
|
| 293 |
+
"note": "Re-scored after fixing a transformers v5 dataloader-caching bug: Trainer.get_eval_dataloader caches under the key 'eval' when a Dataset object is passed and dataloader_persistent_workers=True, so the original in-training test evaluation silently re-scored the validation split. evaluate_split now uses Trainer.predict and verifies the returned labels.",
|
| 294 |
+
"superseded_test_f1": 0.8671882190520018,
|
| 295 |
+
"corrected_test_f1": 0.8805450993472149,
|
| 296 |
+
"validation_f1_unchanged": 0.8671882190520018
|
| 297 |
+
}
|
| 298 |
+
}
|
model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1bb8855bff29d33fc1d8d32f58798090a0e1ce3844d01de4c4c7774367caf59e
|
| 3 |
+
size 500972120
|
tokenizer.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
tokenizer_config.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"add_prefix_space": false,
|
| 3 |
+
"backend": "tokenizers",
|
| 4 |
+
"bos_token": "<s>",
|
| 5 |
+
"cls_token": "<s>",
|
| 6 |
+
"eos_token": "</s>",
|
| 7 |
+
"errors": "replace",
|
| 8 |
+
"is_local": false,
|
| 9 |
+
"local_files_only": false,
|
| 10 |
+
"mask_token": "<mask>",
|
| 11 |
+
"model_max_length": 512,
|
| 12 |
+
"pad_token": "<pad>",
|
| 13 |
+
"sep_token": "</s>",
|
| 14 |
+
"tokenizer_class": "RobertaTokenizer",
|
| 15 |
+
"trim_offsets": true,
|
| 16 |
+
"unk_token": "<unk>"
|
| 17 |
+
}
|
training_args.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fcb3040d4b2288a3bef88cf246696bb9bf7e83e565d1e69fc374507038fc06b1
|
| 3 |
+
size 5201
|
training_config.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"dataset_name": "PoolC/1-fold-clone-detection-600k-5fold",
|
| 3 |
+
"train_split": "train",
|
| 4 |
+
"heldout_split": "val",
|
| 5 |
+
"test_group_fraction": 0.5,
|
| 6 |
+
"max_train_samples": 50000,
|
| 7 |
+
"max_eval_samples": 20000,
|
| 8 |
+
"max_test_samples": 20000,
|
| 9 |
+
"balance_subsamples": true,
|
| 10 |
+
"model_name_or_path": "microsoft/graphcodebert-base",
|
| 11 |
+
"code_length": 512,
|
| 12 |
+
"data_flow_length": 128,
|
| 13 |
+
"attn_implementation": "sdpa",
|
| 14 |
+
"learning_rate": 2e-05,
|
| 15 |
+
"num_train_epochs": 3.0,
|
| 16 |
+
"per_device_train_batch_size": 16,
|
| 17 |
+
"per_device_eval_batch_size": 32,
|
| 18 |
+
"gradient_accumulation_steps": 1,
|
| 19 |
+
"weight_decay": 0.01,
|
| 20 |
+
"warmup_ratio": 0.1,
|
| 21 |
+
"max_grad_norm": 1.0,
|
| 22 |
+
"fp16": true,
|
| 23 |
+
"bf16": false,
|
| 24 |
+
"gradient_checkpointing": false,
|
| 25 |
+
"optim": "adamw_torch",
|
| 26 |
+
"lr_scheduler_type": "linear",
|
| 27 |
+
"class_weighting": "auto",
|
| 28 |
+
"class_weight_threshold": 0.6,
|
| 29 |
+
"eval_strategy": "steps",
|
| 30 |
+
"eval_steps": 1000,
|
| 31 |
+
"save_strategy": "steps",
|
| 32 |
+
"save_steps": 1000,
|
| 33 |
+
"save_total_limit": 2,
|
| 34 |
+
"logging_steps": 100,
|
| 35 |
+
"metric_for_best_model": "f1",
|
| 36 |
+
"greater_is_better": true,
|
| 37 |
+
"load_best_model_at_end": true,
|
| 38 |
+
"seed": 42,
|
| 39 |
+
"full_determinism": false,
|
| 40 |
+
"dataloader_num_workers": 4,
|
| 41 |
+
"preprocessing_num_workers": 8,
|
| 42 |
+
"output_dir": "./outputs",
|
| 43 |
+
"model_dir": "./models/graphcodebert-clone-detection",
|
| 44 |
+
"logging_dir": "./logs",
|
| 45 |
+
"cache_dir": "./outputs/feature_cache",
|
| 46 |
+
"report_to": "none",
|
| 47 |
+
"run_sanity_check": true,
|
| 48 |
+
"sanity_check_samples": 64,
|
| 49 |
+
"total_sequence_length": 640,
|
| 50 |
+
"effective_batch_size": 16
|
| 51 |
+
}
|