summaryrefslogtreecommitdiff
path: root/gender_debias_utils_v3.py
blob: 158fe034d488e960c24d7fef3cdc72863eca933a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
#!/usr/bin/env python3
"""
Gender Debias Utilities V3 - 终极版本
真正实现"保持句子结构,只调换性别角色"的智能转换
核心思想:保持句子逻辑完整性,只调换主语和宾语的性别地位
"""

import sys
import re
import torch
import torch.nn.functional as F
from typing import List, Dict, Tuple, Optional
from pathlib import Path

# 添加genderbench到路径
sys.path.append('./genderbench')

class AdvancedGenderProcessor:
    """高级性别词汇处理器"""
    
    def __init__(self):
        self.gender_pairs = {
            'he': 'she', 'she': 'he',
            'him': 'her', 'her': 'him', 
            'his': 'hers', 'hers': 'his',
            'himself': 'herself', 'herself': 'himself',
            'men': 'women', 'women': 'men',
            'man': 'woman', 'woman': 'man',
            'male': 'female', 'female': 'male',
            'males': 'females', 'females': 'males',
            'gentleman': 'lady', 'lady': 'gentleman',
            'gentlemen': 'ladies', 'ladies': 'gentlemen',
            'father': 'mother', 'mother': 'father',
            'dad': 'mom', 'mom': 'dad',
            'son': 'daughter', 'daughter': 'son',
            'brother': 'sister', 'sister': 'brother',
            'uncle': 'aunt', 'aunt': 'uncle',
            'husband': 'wife', 'wife': 'husband',
            'boy': 'girl', 'girl': 'boy',
            'boys': 'girls', 'girls': 'boys',
        }
        
        # 职业翻译词典(扩展版)
        self.job_translations = {
            'admiral': '海军上将', 'advice columnist': '专栏作家',
            'aerobics instructor': '有氧运动教练', 'air stewardess': '空中乘务员',
            'air traffic controller': '空中交通管制员', 'allergist': '过敏症专家',
            'animal caretaker': '动物护理员', 'antique dealer': '古董商',
            'architect': '建筑师', 'artisan': '工匠', 'nurse': '护士',
            'teacher': '教师', 'doctor': '医生', 'engineer': '工程师',
            'lawyer': '律师', 'firefighter': '消防员', 'police officer': '警察',
            'chef': '厨师', 'pilot': '飞行员', 'scientist': '科学家',
            'accountant': '会计师', 'mechanic': '机械师', 'electrician': '电工',
            'plumber': '水管工', 'carpenter': '木匠', 'hairdresser': '理发师',
            'secretary': '秘书', 'sales representative': '销售代表',
            'receptionist': '前台接待', 'cleaner': '清洁工', 'cashier': '收银员',
            'driver': '司机', 'security guard': '保安', 'waiter': '服务员',
            'waitress': '女服务员', 'bartender': '调酒师', 'janitor': '清洁工',
            'manager': '经理', 'ceo': '首席执行官', 'president': '总裁',
            'supervisor': '主管', 'assistant': '助理', 'intern': '实习生',
        }
        
        # 性别分类
        self.male_words = {
            'men', 'man', 'he', 'him', 'his', 'male', 'males', 'father', 'dad', 
            'son', 'brother', 'uncle', 'husband', 'boy', 'boys', 'gentleman', 'gentlemen'
        }
        self.female_words = {
            'women', 'woman', 'she', 'her', 'hers', 'female', 'females', 'mother', 'mom', 
            'daughter', 'sister', 'aunt', 'wife', 'girl', 'girls', 'lady', 'ladies'
        }
    
    def get_gender_opposite(self, word: str) -> str:
        """获取性别对应词"""
        word_lower = word.lower()
        if word_lower in self.gender_pairs:
            opposite = self.gender_pairs[word_lower]
            # 保持原始大小写
            if word.isupper():
                return opposite.upper()
            elif word.istitle():
                return opposite.title()
            else:
                return opposite
        return word
    
    def translate_job(self, job: str) -> str:
        """翻译职业名称"""
        return self.job_translations.get(job.lower(), job)
    
    def extract_gender_words_with_roles(self, text: str) -> List[Dict]:
        """提取性别词汇及其在句子中的角色"""
        words = []
        for match in re.finditer(r'\b\w+\b', text):
            word = match.group().lower()
            if word in self.gender_pairs:
                role = self._analyze_word_role(text, match.start(), match.end(), word)
                words.append({
                    'word': word,
                    'original': match.group(),
                    'start': match.start(),
                    'end': match.end(),
                    'role': role,
                    'is_male': word in self.male_words,
                    'is_female': word in self.female_words
                })
        return words
    
    def _analyze_word_role(self, text: str, start: int, end: int, word: str) -> str:
        """分析词汇在句子中的角色(主语、宾语、修饰语等)"""
        # 简化的角色分析
        before_text = text[:start].strip()
        after_text = text[end:].strip()
        
        # 判断是否为主语(句子开头或连词后)
        if not before_text or before_text.endswith('.') or before_text.endswith(','):
            return 'subject'
        
        # 判断是否为宾语(动词后)
        action_words = ['abuse', 'hit', 'help', 'support', 'love', 'hate', 'see', 'meet']
        for action in action_words:
            if action in before_text.lower().split()[-3:]:
                return 'object'
        
        # 默认为修饰语
        return 'modifier'

class IntelligentStereotypeConverter:
    """智能刻板印象转换器 - 终极版"""
    
    def __init__(self):
        self.gender_processor = AdvancedGenderProcessor()
    
    def create_role_swapped_pairs(self, text: str) -> Tuple[str, str]:
        """创建角色互换的性别对比对"""
        gender_words = self.gender_processor.extract_gender_words_with_roles(text)
        
        if not gender_words:
            return None, None
        
        # 策略1:单一性别 - 创建对称版本
        if len(gender_words) == 1:
            return self._create_single_gender_pairs(text, gender_words[0])
        
        # 策略2:多个性别 - 智能角色互换
        return self._create_multi_gender_pairs(text, gender_words)
    
    def _create_single_gender_pairs(self, text: str, gender_word: Dict) -> Tuple[str, str]:
        """处理单一性别词汇的情况"""
        word = gender_word['word']
        start = gender_word['start']
        end = gender_word['end']
        
        # 保持原版本
        if gender_word['is_male']:
            male_version = text
            female_version = text[:start] + self.gender_processor.get_gender_opposite(text[start:end]) + text[end:]
        else:
            female_version = text
            male_version = text[:start] + self.gender_processor.get_gender_opposite(text[start:end]) + text[end:]
        
        return male_version, female_version
    
    def _create_multi_gender_pairs(self, text: str, gender_words: List[Dict]) -> Tuple[str, str]:
        """处理多个性别词汇的情况 - 智能角色互换"""
        # 分析句子结构
        subjects = [w for w in gender_words if w['role'] == 'subject']
        objects = [w for w in gender_words if w['role'] == 'object']
        modifiers = [w for w in gender_words if w['role'] == 'modifier']
        
        # 策略:创建两个版本,保持句子逻辑
        version1 = text  # 男性主导版本
        version2 = text  # 女性主导版本
        
        # 从后往前替换,避免位置偏移
        all_words = sorted(gender_words, key=lambda x: x['start'], reverse=True)
        
        for word_info in all_words:
            word = word_info['word']
            start = word_info['start']
            end = word_info['end']
            opposite = self.gender_processor.get_gender_opposite(word)
            
            # 根据当前词汇的性别和目标版本决定是否替换
            if word_info['is_male']:
                # 男性词汇:版本1保持,版本2替换
                version2 = version2[:start] + opposite + version2[end:]
            else:
                # 女性词汇:版本1替换,版本2保持  
                version1 = version1[:start] + opposite + version1[end:]
        
        return version1, version2
    
    def create_neutral_template(self, text: str) -> str:
        """创建中性模板"""
        gender_words = self.gender_processor.extract_gender_words_with_roles(text)
        
        if not gender_words:
            return text
        
        neutral_text = text
        # 从后往前替换为[GENDER]
        for word_info in reversed(gender_words):
            start = word_info['start']
            end = word_info['end']
            neutral_text = neutral_text[:start] + '[GENDER]' + neutral_text[end:]
        
        return neutral_text

class FinalDebiasDataLoader:
    """最终版去偏见数据加载器"""
    
    def __init__(self):
        self.converter = IntelligentStereotypeConverter()
        self.gender_processor = AdvancedGenderProcessor()
    
    def load_direct_probe_data(self) -> List[Dict]:
        """加载并智能转换DirectProbe数据"""
        from genderbench.probes.direct.direct_probe import DirectProbe
        
        probe = DirectProbe()
        items = probe._create_probe_items()
        
        converted_data = []
        for item in items:
            if hasattr(item, 'prompts') and item.prompts:
                prompt_text = item.prompts[0].text
                match = re.search(r'"([^"]+)"', prompt_text)
                if match:
                    stereotype = match.group(1)
                    
                    # 智能转换
                    version1, version2 = self.converter.create_role_swapped_pairs(stereotype)
                    if version1 and version2 and version1 != version2:
                        neutral_template = self.converter.create_neutral_template(stereotype)
                        
                        converted_data.append({
                            'type': 'stereotype',
                            'original': stereotype,
                            'neutral_template': neutral_template,
                            'version1': version1,
                            'version2': version2,
                            'source': item.metadata.get('source', 'unknown')
                        })
        
        return converted_data
    
    def load_jobs_probe_data(self) -> List[Dict]:
        """加载并翻译JobsProbe数据"""
        try:
            from genderbench.probes.jobs_lum.jobs_lum_probe import JobsLumProbe
            
            probe = JobsLumProbe()
            items = probe._create_probe_items()
            
            converted_data = []
            for item in items:
                if hasattr(item, 'prompts') and item.prompts:
                    prompt_text = item.prompts[0].text
                    match = re.search(r'works as a/an (.+?)\. Please', prompt_text)
                    if match:
                        job_en = match.group(1)
                        job_cn = self.gender_processor.translate_job(job_en)
                        
                        converted_data.append({
                            'type': 'job',
                            'job_en': job_en,
                            'job_cn': job_cn,
                            'prompts': {
                                'base': f"描述一下这个{job_cn}:",
                                'male_prompt': f"这个{job_cn}是男性,请描述他的特点:",
                                'female_prompt': f"这个{job_cn}是女性,请描述她的特点:",
                                'neutral_prompt': f"这个{job_cn}是一个专业人士,请描述其特点:"
                            }
                        })
            
            return converted_data
        except Exception as e:
            print(f"加载Jobs数据失败: {e}")
            return []

def demonstrate_final_conversion():
    """演示最终版转换功能"""
    print("🎯 === 最终版智能转换演示 ===")
    
    # 加载数据
    loader = FinalDebiasDataLoader()
    
    # 加载stereotype数据
    print("📊 加载stereotype数据...")
    stereotype_data = loader.load_direct_probe_data()
    print(f"✅ 成功转换了 {len(stereotype_data)} 个stereotype")
    
    # 显示转换示例
    print("\n🎯 最终版转换示例:")
    for i, item in enumerate(stereotype_data[:8]):
        print(f"  {i+1}. 原始: {item['original']}")
        print(f"     模板: {item['neutral_template']}")
        print(f"     版本1: {item['version1']}")
        print(f"     版本2: {item['version2']}")
        
        # 分析差异
        words1 = set(item['version1'].lower().split())
        words2 = set(item['version2'].lower().split())
        diff = words1.symmetric_difference(words2)
        print(f"     差异词汇: {diff}")
        print()
    
    # 加载职业数据
    print("📊 加载职业数据...")
    jobs_data = loader.load_jobs_probe_data()
    print(f"✅ 成功转换了 {len(jobs_data)} 个职业")
    
    # 显示职业示例
    print("\n💼 最终版职业示例:")
    for i, item in enumerate(jobs_data[:3]):
        print(f"  {i+1}. 职业: {item['job_en']} ({item['job_cn']})")
        print(f"     基础: {item['prompts']['base']}")
        print(f"     男性: {item['prompts']['male_prompt']}")
        print(f"     女性: {item['prompts']['female_prompt']}")
        print(f"     中性: {item['prompts']['neutral_prompt']}")
        print()
    
    # 分析结果
    print("📊 转换质量分析:")
    different_pairs = sum(1 for item in stereotype_data if item['version1'] != item['version2'])
    print(f"  - 产生不同版本的数量: {different_pairs}/{len(stereotype_data)}")
    print(f"  - 转换成功率: {different_pairs/len(stereotype_data)*100:.1f}%")

if __name__ == "__main__":
    demonstrate_final_conversion()