Behlul commited on
Commit ·
5f0e511
1
Parent(s): 7d91ba4
Refactor: strip all options + produce plain-text answer
Browse files- Remove all option lines from question body (not just distractors)
- Strip leading 'Soru:' / 'Question:' prefix
- Rewrite assistant message to only the correct option's plain text
- More robust answer-letter regex for '**Cevap:** **B) ...**' style
- README.md +16 -15
- filter_correct_answer.py +103 -51
README.md
CHANGED
|
@@ -12,7 +12,7 @@ tags:
|
|
| 12 |
|
| 13 |
# Multiple-Choice Correct-Answer Filter
|
| 14 |
|
| 15 |
-
Çoktan seçmeli SFT dataset'lerinde **
|
| 16 |
|
| 17 |
Fizik, matematik, tarih gibi derslere özgü kurulmamıştır — `conversations` veya `messages` formatındaki herhangi bir JSONL çoktan seçmeli veri setinde çalışır.
|
| 18 |
|
|
@@ -20,24 +20,25 @@ Fizik, matematik, tarih gibi derslere özgü kurulmamıştır — `conversations
|
|
| 20 |
|
| 21 |
**Önce:**
|
| 22 |
```
|
| 23 |
-
Soru: Termodinamiğin birinci yasası neyi ifade eder?
|
| 24 |
-
A) Entropi her zaman artar
|
| 25 |
-
B) Enerji korunur
|
| 26 |
-
C) Mutlak sıcaklık sıfıra ulaşılamaz
|
| 27 |
-
D) Isı soğuktan sıcağa akar
|
| 28 |
|
| 29 |
-
Cevap: B) Enerji korunur
|
| 30 |
```
|
| 31 |
|
| 32 |
-
**Sonra
|
| 33 |
```
|
| 34 |
-
|
| 35 |
-
B) Enerji korunur
|
| 36 |
|
| 37 |
-
|
| 38 |
```
|
| 39 |
|
| 40 |
-
|
|
|
|
|
|
|
| 41 |
|
| 42 |
## Desteklenen formatlar
|
| 43 |
|
|
@@ -47,7 +48,7 @@ Asistan mesajına dokunmaz, sadece soru gövdesindeki distractor'ları siler.
|
|
| 47 |
| `messages` | `[{"role": "user/assistant", "content": "..."}]` |
|
| 48 |
|
| 49 |
Desteklenen cevap prefixleri:
|
| 50 |
-
- Türkçe: `Cevap: B)`, `Doğru cevap: B`, `**Cevap:** B`
|
| 51 |
- İngilizce: `Answer: B`, `The answer is B`, `**Answer:** B`, `B is correct`
|
| 52 |
|
| 53 |
A–Z arası harfler (fizik80k örneklerinde A–J'ye kadar şık olabiliyor).
|
|
@@ -62,7 +63,7 @@ wget https://huggingface.co/MRBeDev/mc-answer-filter/resolve/main/filter_correct
|
|
| 62 |
|
| 63 |
## Kullanım
|
| 64 |
|
| 65 |
-
**1) HF'ten otomatik indir + temizle (
|
| 66 |
|
| 67 |
```bash
|
| 68 |
export HF_TOKEN=hf_... # private dataset için
|
|
@@ -114,7 +115,7 @@ python3 filter_correct_answer.py \
|
|
| 114 |
|
| 115 |
## Dataset bütünlüğü
|
| 116 |
|
| 117 |
-
Default olarak çoktan seçmeli olmayan kayıtlar **olduğu gibi** geçirilir, böylece dataset boyutu korunur. Sadece çoktan seçmeli soruların içi temizlenir. `--drop-non-mc` ile bu davranışı değiştirebilirsin.
|
| 118 |
|
| 119 |
## Lisans
|
| 120 |
|
|
|
|
| 12 |
|
| 13 |
# Multiple-Choice Correct-Answer Filter
|
| 14 |
|
| 15 |
+
Çoktan seçmeli SFT dataset'lerinde **şıkları tamamen kaldırıp soruyu saf metne indiren ve cevap mesajını doğru şıkkın düz metnine çeviren** küçük bir Python aracı.
|
| 16 |
|
| 17 |
Fizik, matematik, tarih gibi derslere özgü kurulmamıştır — `conversations` veya `messages` formatındaki herhangi bir JSONL çoktan seçmeli veri setinde çalışır.
|
| 18 |
|
|
|
|
| 20 |
|
| 21 |
**Önce:**
|
| 22 |
```
|
| 23 |
+
user: Soru: Termodinamiğin birinci yasası neyi ifade eder?
|
| 24 |
+
A) Entropi her zaman artar
|
| 25 |
+
B) Enerji korunur
|
| 26 |
+
C) Mutlak sıcaklık sıfıra ulaşılamaz
|
| 27 |
+
D) Isı soğuktan sıcağa akar
|
| 28 |
|
| 29 |
+
assistant: Cevap: B) Enerji korunur
|
| 30 |
```
|
| 31 |
|
| 32 |
+
**Sonra:**
|
| 33 |
```
|
| 34 |
+
user: Termodinamiğin birinci yasası neyi ifade eder?
|
|
|
|
| 35 |
|
| 36 |
+
assistant: Enerji korunur
|
| 37 |
```
|
| 38 |
|
| 39 |
+
- Soru metnindeki tüm şık satırları ve "Soru:" / "Question:" prefix'i temizlenir.
|
| 40 |
+
- Cevap mesajı, doğru şıkkın harf/prefix'siz düz metnine dönüşür.
|
| 41 |
+
- Serbest metinli (çoktan seçmeli olmayan) kayıtlar varsayılan olarak olduğu gibi geçirilir.
|
| 42 |
|
| 43 |
## Desteklenen formatlar
|
| 44 |
|
|
|
|
| 48 |
| `messages` | `[{"role": "user/assistant", "content": "..."}]` |
|
| 49 |
|
| 50 |
Desteklenen cevap prefixleri:
|
| 51 |
+
- Türkçe: `Cevap: B)`, `Doğru cevap: B`, `**Cevap:** **B) ...**`
|
| 52 |
- İngilizce: `Answer: B`, `The answer is B`, `**Answer:** B`, `B is correct`
|
| 53 |
|
| 54 |
A–Z arası harfler (fizik80k örneklerinde A–J'ye kadar şık olabiliyor).
|
|
|
|
| 63 |
|
| 64 |
## Kullanım
|
| 65 |
|
| 66 |
+
**1) HF'ten otomatik indir + temizle (tüm split'ler birden):**
|
| 67 |
|
| 68 |
```bash
|
| 69 |
export HF_TOKEN=hf_... # private dataset için
|
|
|
|
| 115 |
|
| 116 |
## Dataset bütünlüğü
|
| 117 |
|
| 118 |
+
Default olarak çoktan seçmeli olmayan kayıtlar **olduğu gibi** geçirilir, böylece dataset boyutu korunur. Sadece çoktan seçmeli soruların içi temizlenir ve cevap mesajı sadeleştirilir. `--drop-non-mc` ile bu davranışı değiştirebilirsin.
|
| 119 |
|
| 120 |
## Lisans
|
| 121 |
|
filter_correct_answer.py
CHANGED
|
@@ -1,9 +1,22 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
-
HF dataset temizleyici: Çoktan seçmeli sorulardan
|
|
|
|
| 4 |
|
| 5 |
Hazırlayan: Behlül
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
Desteklenen dataset formatları (fizik, tarih, matematik için aynı script):
|
| 8 |
- conversations: [{"from": "human/gpt", "value": "..."}]
|
| 9 |
- messages: [{"role": "user/assistant", "content": "..."}]
|
|
@@ -41,16 +54,17 @@ OPTION_LINE_RE = re.compile(
|
|
| 41 |
re.MULTILINE,
|
| 42 |
)
|
| 43 |
|
| 44 |
-
# Cevap harfini çıkarmak için pattern'lar (öncelik sırasına göre)
|
|
|
|
| 45 |
ANSWER_PATTERNS = [
|
| 46 |
-
# "Cevap: B)" / "Cevap: **B**" / "Cevap
|
| 47 |
-
re.compile(r'[Cc]evap\s*[:\-]\s
|
| 48 |
# "Doğru cevap: B"
|
| 49 |
-
re.compile(r'[Dd]o[ğg]ru\s+cevap\s*[:\-]\s
|
| 50 |
-
# "**Answer:** B" / "Answer: B"
|
| 51 |
-
re.compile(r'\
|
| 52 |
# "The answer is B" / "the correct answer is B"
|
| 53 |
-
re.compile(r'[Tt]he\s+(?:correct\s+)?answer\s+is\s*
|
| 54 |
# "**B** is correct" / "B is the correct answer"
|
| 55 |
re.compile(r'\*{0,2}([A-Z])\*{0,2}\s+is\s+(?:the\s+)?correct'),
|
| 56 |
# Son çare: metin sonunda tek başına duran tek büyük harf
|
|
@@ -69,53 +83,86 @@ def extract_correct_letter(answer_text: str) -> str | None:
|
|
| 69 |
return None
|
| 70 |
|
| 71 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
def find_option_letters(question: str) -> set[str]:
|
| 73 |
"""Sorudaki seçenek harflerini bulur (ör. {'A','B','C','D'})."""
|
| 74 |
return {m.upper() for m in OPTION_LINE_RE.findall(question)}
|
| 75 |
|
| 76 |
|
| 77 |
-
def
|
| 78 |
"""
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
"""
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
if m:
|
|
|
|
| 92 |
current_letter = m.group(1).upper()
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
|
|
|
|
|
|
| 96 |
else:
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
# devam satırı mı yoksa normal soru metni mi?
|
| 101 |
-
is_blank = line.strip() == ''
|
| 102 |
-
if current_letter is None:
|
| 103 |
-
# soru gövdesi — her zaman tut
|
| 104 |
-
out.append(line)
|
| 105 |
-
else:
|
| 106 |
-
# bir şıkkın devamı
|
| 107 |
-
if is_blank:
|
| 108 |
-
# boş satır şıkkın bittiğini gösterir
|
| 109 |
-
current_letter = None
|
| 110 |
-
out.append(line)
|
| 111 |
-
elif current_letter == correct_letter:
|
| 112 |
-
out.append(line)
|
| 113 |
-
# else: yanlış şıkkın devam satırı — at
|
| 114 |
-
# Cevap: / Answer: gibi ipuçları gelirse şıkkı bitir
|
| 115 |
-
if re.match(r'^\s*\*{0,2}\s*(Cevap|Answer|Doğru cevap)\b', line, re.IGNORECASE):
|
| 116 |
-
current_letter = None
|
| 117 |
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
|
| 121 |
# ---------- Kayıt işleme ----------
|
|
@@ -161,26 +208,31 @@ def process_record(rec: dict) -> tuple[dict, bool, str]:
|
|
| 161 |
if not isinstance(question, str) or not isinstance(answer, str):
|
| 162 |
return rec, False, 'non_string_content'
|
| 163 |
|
| 164 |
-
|
| 165 |
-
if len(
|
| 166 |
return rec, False, 'not_multichoice'
|
| 167 |
|
| 168 |
correct_letter = extract_correct_letter(answer)
|
| 169 |
if not correct_letter:
|
| 170 |
return rec, False, 'no_correct_letter'
|
| 171 |
|
| 172 |
-
|
|
|
|
| 173 |
return rec, False, f'answer_letter_{correct_letter}_not_in_options'
|
| 174 |
|
| 175 |
-
new_question
|
| 176 |
-
|
| 177 |
-
|
|
|
|
|
|
|
| 178 |
|
| 179 |
-
# Immutable kopya
|
| 180 |
new_rec = dict(rec)
|
| 181 |
new_msgs = [dict(m) for m in msgs]
|
| 182 |
new_msgs[user_idx] = dict(new_msgs[user_idx])
|
| 183 |
new_msgs[user_idx][content_key] = new_question
|
|
|
|
|
|
|
| 184 |
|
| 185 |
if 'conversations' in rec:
|
| 186 |
new_rec['conversations'] = new_msgs
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
+
HF dataset temizleyici: Çoktan seçmeli sorulardan şıkları tamamen kaldırır,
|
| 4 |
+
soruyu saf metne indirir ve cevap mesajını doğru şıkkın düz metnine çevirir.
|
| 5 |
|
| 6 |
Hazırlayan: Behlül
|
| 7 |
|
| 8 |
+
Girdi örneği:
|
| 9 |
+
user: "Soru: Termodinamiğin birinci yasası neyi ifade eder?
|
| 10 |
+
A) Kütle korunur
|
| 11 |
+
B) Enerji korunur
|
| 12 |
+
C) Entropi azalır
|
| 13 |
+
D) Hiçbiri"
|
| 14 |
+
assistant: "Cevap: B) Enerji korunur"
|
| 15 |
+
|
| 16 |
+
Çıktı:
|
| 17 |
+
user: "Termodinamiğin birinci yasası neyi ifade eder?"
|
| 18 |
+
assistant: "Enerji korunur"
|
| 19 |
+
|
| 20 |
Desteklenen dataset formatları (fizik, tarih, matematik için aynı script):
|
| 21 |
- conversations: [{"from": "human/gpt", "value": "..."}]
|
| 22 |
- messages: [{"role": "user/assistant", "content": "..."}]
|
|
|
|
| 54 |
re.MULTILINE,
|
| 55 |
)
|
| 56 |
|
| 57 |
+
# Cevap harfini çıkarmak için pattern'lar (öncelik sırasına göre).
|
| 58 |
+
# "[\s\*]*" ile aradaki birden fazla bold işareti / whitespace kombinasyonu esnek yakalanır.
|
| 59 |
ANSWER_PATTERNS = [
|
| 60 |
+
# "Cevap: B)" / "Cevap: **B**" / "**Cevap:** **B) ..."
|
| 61 |
+
re.compile(r'[Cc]evap\s*[:\-][\s\*]*([A-Z])\b'),
|
| 62 |
# "Doğru cevap: B"
|
| 63 |
+
re.compile(r'[Dd]o[ğg]ru\s+cevap\s*[:\-][\s\*]*([A-Z])\b'),
|
| 64 |
+
# "Answer: B" / "**Answer:** B" / "**Answer:** **B**"
|
| 65 |
+
re.compile(r'[\s\*]*[Aa]nswer[\s\*]*[:\-][\s\*]*([A-Z])\b'),
|
| 66 |
# "The answer is B" / "the correct answer is B"
|
| 67 |
+
re.compile(r'[Tt]he\s+(?:correct\s+)?answer\s+is[\s\*:\-]*([A-Z])\b'),
|
| 68 |
# "**B** is correct" / "B is the correct answer"
|
| 69 |
re.compile(r'\*{0,2}([A-Z])\*{0,2}\s+is\s+(?:the\s+)?correct'),
|
| 70 |
# Son çare: metin sonunda tek başına duran tek büyük harf
|
|
|
|
| 83 |
return None
|
| 84 |
|
| 85 |
|
| 86 |
+
# Şık satırı başlığı + metni aynı regex ile yakala ("B) Enerji korunur" → ("B", "Enerji korunur"))
|
| 87 |
+
OPTION_HEADER_RE = re.compile(
|
| 88 |
+
r'^\s*\*{0,2}\s*([A-Z])\s*\*{0,2}\s*[\)\.\-]\s*(.*)$'
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
# Soru metninin başındaki "Soru:" / "Question:" etiketi
|
| 92 |
+
QUESTION_PREFIX_RE = re.compile(
|
| 93 |
+
r'^\s*\*{0,2}\s*(?:Soru|Question|Q)\s*\*{0,2}\s*[:\-]\s*',
|
| 94 |
+
re.IGNORECASE,
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
|
| 98 |
def find_option_letters(question: str) -> set[str]:
|
| 99 |
"""Sorudaki seçenek harflerini bulur (ör. {'A','B','C','D'})."""
|
| 100 |
return {m.upper() for m in OPTION_LINE_RE.findall(question)}
|
| 101 |
|
| 102 |
|
| 103 |
+
def parse_options(question: str) -> dict[str, str]:
|
| 104 |
"""
|
| 105 |
+
Sorudaki tüm şıkları {harf: metin} sözlüğü olarak döndürür.
|
| 106 |
+
Şıkın birden fazla satıra yayılması durumunda devam satırlarını
|
| 107 |
+
cümlenin bir parçası olarak birleştirir.
|
| 108 |
"""
|
| 109 |
+
options: dict[str, str] = {}
|
| 110 |
+
current_letter: str | None = None
|
| 111 |
+
current_lines: list[str] = []
|
| 112 |
+
|
| 113 |
+
def flush():
|
| 114 |
+
nonlocal current_letter, current_lines
|
| 115 |
+
if current_letter is not None:
|
| 116 |
+
options[current_letter] = '\n'.join(current_lines).strip()
|
| 117 |
+
current_letter = None
|
| 118 |
+
current_lines = []
|
| 119 |
+
|
| 120 |
+
for line in question.split('\n'):
|
| 121 |
+
m = OPTION_HEADER_RE.match(line)
|
| 122 |
if m:
|
| 123 |
+
flush()
|
| 124 |
current_letter = m.group(1).upper()
|
| 125 |
+
current_lines = [m.group(2)]
|
| 126 |
+
continue
|
| 127 |
+
if current_letter is not None:
|
| 128 |
+
if line.strip() == '':
|
| 129 |
+
flush()
|
| 130 |
else:
|
| 131 |
+
current_lines.append(line)
|
| 132 |
+
flush()
|
| 133 |
+
return options
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
|
| 135 |
+
|
| 136 |
+
def clean_question_text(question: str) -> str:
|
| 137 |
+
"""Soru gövdesini çıkarır — tüm şık satırlarını ve 'Soru:' prefix'ini temizler."""
|
| 138 |
+
lines = question.split('\n')
|
| 139 |
+
body: list[str] = []
|
| 140 |
+
in_option_block = False
|
| 141 |
+
for line in lines:
|
| 142 |
+
if OPTION_HEADER_RE.match(line):
|
| 143 |
+
in_option_block = True
|
| 144 |
+
continue
|
| 145 |
+
if in_option_block:
|
| 146 |
+
# Boş satır şık bloğunu bitirir; devam satırları atılır
|
| 147 |
+
if line.strip() == '':
|
| 148 |
+
in_option_block = False
|
| 149 |
+
continue
|
| 150 |
+
body.append(line)
|
| 151 |
+
|
| 152 |
+
text = '\n'.join(body)
|
| 153 |
+
text = QUESTION_PREFIX_RE.sub('', text, count=1)
|
| 154 |
+
text = re.sub(r'\n{3,}', '\n\n', text).strip()
|
| 155 |
+
return text
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def clean_option_text(text: str) -> str:
|
| 159 |
+
"""Şık metninden başlangıçtaki harf prefix'i, bold işaretleri ve artık noktalamayı temizler."""
|
| 160 |
+
t = text.strip()
|
| 161 |
+
# Yineli prefix: "**B)** Enerji korunur" gibi
|
| 162 |
+
t = re.sub(r'^\*{0,2}\s*[A-Z]\s*\*{0,2}\s*[\)\.\-]\s*', '', t)
|
| 163 |
+
t = t.replace('**', '').strip()
|
| 164 |
+
t = t.rstrip('.').strip()
|
| 165 |
+
return t
|
| 166 |
|
| 167 |
|
| 168 |
# ---------- Kayıt işleme ----------
|
|
|
|
| 208 |
if not isinstance(question, str) or not isinstance(answer, str):
|
| 209 |
return rec, False, 'non_string_content'
|
| 210 |
|
| 211 |
+
options = parse_options(question)
|
| 212 |
+
if len(options) < 2:
|
| 213 |
return rec, False, 'not_multichoice'
|
| 214 |
|
| 215 |
correct_letter = extract_correct_letter(answer)
|
| 216 |
if not correct_letter:
|
| 217 |
return rec, False, 'no_correct_letter'
|
| 218 |
|
| 219 |
+
correct_text = options.get(correct_letter)
|
| 220 |
+
if not correct_text:
|
| 221 |
return rec, False, f'answer_letter_{correct_letter}_not_in_options'
|
| 222 |
|
| 223 |
+
new_question = clean_question_text(question)
|
| 224 |
+
new_answer = clean_option_text(correct_text)
|
| 225 |
+
|
| 226 |
+
if not new_question.strip() or not new_answer.strip():
|
| 227 |
+
return rec, False, 'empty_after_clean'
|
| 228 |
|
| 229 |
+
# Immutable kopya — hem soru hem cevap mesajı güncellenir
|
| 230 |
new_rec = dict(rec)
|
| 231 |
new_msgs = [dict(m) for m in msgs]
|
| 232 |
new_msgs[user_idx] = dict(new_msgs[user_idx])
|
| 233 |
new_msgs[user_idx][content_key] = new_question
|
| 234 |
+
new_msgs[asst_idx] = dict(new_msgs[asst_idx])
|
| 235 |
+
new_msgs[asst_idx][content_key] = new_answer
|
| 236 |
|
| 237 |
if 'conversations' in rec:
|
| 238 |
new_rec['conversations'] = new_msgs
|