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

Files changed (2) hide show
  1. README.md +16 -15
  2. 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 **sorulardaki yanlış şıkları silip sadece doğru cevabı bırakan** 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,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 (filtre uygulanmış):**
33
  ```
34
- Soru: Termodinamiğin birinci yasası neyi ifade eder?
35
- B) Enerji korunur
36
 
37
- Cevap: B) Enerji korunur
38
  ```
39
 
40
- Asistan mesajına dokunmaz, sadece soru gövdesindeki distractor'ları siler.
 
 
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 (3 split birden):**
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 doğru cevap dışındaki şıkları siler.
 
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 : B."
47
- re.compile(r'[Cc]evap\s*[:\-]\s*\*{0,2}\s*([A-Z])\b'),
48
  # "Doğru cevap: B"
49
- re.compile(r'[Dd]o[ğg]ru\s+cevap\s*[:\-]\s*\*{0,2}\s*([A-Z])\b'),
50
- # "**Answer:** B" / "Answer: B"
51
- re.compile(r'\*{0,2}\s*[Aa]nswer\s*\*{0,2}\s*[:\-]\s*\*{0,2}\s*([A-Z])\b'),
52
  # "The answer is B" / "the correct answer is B"
53
- re.compile(r'[Tt]he\s+(?:correct\s+)?answer\s+is\s*[:\-]?\s*\*{0,2}\s*([A-Z])\b'),
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 strip_distractors(question: str, correct_letter: str) -> tuple[str, bool]:
78
  """
79
- Sorudan doğru şık dışındaki bütün seçenek satırlarını siler.
80
- Aynı seçenek birden fazla satıra yayılabileceği için "bu seçenek bloğu
81
- hangi harfle başladı" state'ini takip ediyoruz.
82
  """
83
- lines = question.split('\n')
84
- out = []
85
- current_letter = None # şu anda hangi şıkkın içindeyiz
86
- removed_any = False
87
- kept_correct = False
88
-
89
- for line in lines:
90
- m = re.match(r'^\s*\*{0,2}\s*([A-Z])\s*\*{0,2}\s*[\)\.\-]\s*', line)
 
 
 
 
 
91
  if m:
 
92
  current_letter = m.group(1).upper()
93
- if current_letter == correct_letter:
94
- out.append(line)
95
- kept_correct = True
 
 
96
  else:
97
- removed_any = True
98
- # skip (doğru şık değil)
99
- else:
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
- return '\n'.join(out), (removed_any and kept_correct)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- option_letters = find_option_letters(question)
165
- if len(option_letters) < 2:
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
- if correct_letter not in option_letters:
 
173
  return rec, False, f'answer_letter_{correct_letter}_not_in_options'
174
 
175
- new_question, changed = strip_distractors(question, correct_letter)
176
- if not changed:
177
- return rec, False, 'no_distractors_stripped'
 
 
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