File size: 16,367 Bytes
76285c7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Build the defence deck as .pptx from the same content as the Beamer source.

Beamer is the primary deliverable; this exists so the deck can be handed to
someone who needs to edit it in PowerPoint. Numbers come from
slides/numbers.json (generated), figures from slides/figures/ (generated), so
the two decks cannot disagree with each other or with the thesis.
"""
from __future__ import annotations

import json
from pathlib import Path

from pptx import Presentation
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN
from pptx.util import Emu, Inches, Pt

ROOT = Path(__file__).resolve().parents[1]
SLIDES = ROOT / 'slides'
FIGS = SLIDES / 'figures'

INK = RGBColor(0x0B, 0x0B, 0x0B)
INK2 = RGBColor(0x52, 0x51, 0x4E)
MUTED = RGBColor(0x89, 0x87, 0x81)
MINORITY = RGBColor(0x2A, 0x78, 0xD6)
SURFACE = RGBColor(0xFC, 0xFC, 0xFB)

N = json.loads((SLIDES / 'numbers.json').read_text())
W, H = Inches(13.333), Inches(7.5)


def background(slide):
    fill = slide.background.fill
    fill.solid()
    fill.fore_color.rgb = SURFACE


def textbox(slide, left, top, width, height, text, size=18, color=INK,
            bold=False, align=PP_ALIGN.LEFT, spacing=1.15):
    box = slide.shapes.add_textbox(left, top, width, height)
    frame = box.text_frame
    frame.word_wrap = True
    for i, line in enumerate(text.split('\n')):
        para = frame.paragraphs[0] if i == 0 else frame.add_paragraph()
        para.alignment = align
        para.line_spacing = spacing
        run = para.add_run()
        run.text = line
        run.font.size = Pt(size)
        run.font.color.rgb = color
        run.font.bold = bold
        run.font.name = 'Calibri'
    return box


def blank(prs):
    slide = prs.slides.add_slide(prs.slide_layouts[6])
    background(slide)
    return slide


def titled(prs, title):
    slide = blank(prs)
    textbox(slide, Inches(0.7), Inches(0.4), Inches(12.0), Inches(0.9),
            title, size=30, color=INK, bold=True)
    return slide


def picture(slide, name, top=Inches(1.45), height=Inches(4.6)):
    path = FIGS / f'{name}.png'
    pic = slide.shapes.add_picture(str(path), Inches(0), top, height=height)
    pic.left = Emu(int((W - pic.width) / 2))
    return pic


def takeaway(slide, text, size=16):
    textbox(slide, Inches(0.7), Inches(6.25), Inches(12.0), Inches(1.0),
            text, size=size, color=INK2)


def bullets(slide, items, top=Inches(1.6), size=20, gap=0.78):
    for i, item in enumerate(items):
        textbox(slide, Inches(1.0), top + Inches(gap * i), Inches(11.4),
                Inches(0.9), f'•  {item}', size=size, color=INK)


def build():
    prs = Presentation()
    prs.slide_width, prs.slide_height = W, H

    s = blank(prs)
    textbox(s, Inches(1.0), Inches(2.5), Inches(11.3), Inches(2.0),
            'Majority Accumulation and Minority False Matches\nin Locally Adapted Face Verification',
            size=34, color=INK, bold=True, align=PP_ALIGN.CENTER)
    textbox(s, Inches(1.0), Inches(4.6), Inches(11.3), Inches(0.6),
            'MSc thesis defence', size=18, color=MUTED, align=PP_ALIGN.CENTER)

    s = titled(prs, 'Institutions adapt recognisers. They do not train them.')
    bullets(s, ['Take a model pretrained on a very large public corpus.',
                'Fine-tune it on the identities you have already enrolled.',
                'Cheap, needs no new data, and reliably improves measured accuracy.'])
    textbox(s, Inches(0.7), Inches(4.5), Inches(12.0), Inches(1.4),
            f"But you do not choose who is in that set. In our corpus, "
            f"{N['pctMale']}% of {N['Nidentities']} enrolled identities are male — "
            f"not by decision, but because the cohort comes from engineering programmes.",
            size=19, color=INK)
    takeaway(s, 'The practitioner faces a fork that does not look like a fork: '
                'use the data as it arrives, or curate it.')

    s = titled(prs, 'The question')
    textbox(s, Inches(1.0), Inches(2.1), Inches(11.3), Inches(1.4),
            'What does the natural choice cost,\nand who pays for it?',
            size=32, color=MINORITY, bold=True, align=PP_ALIGN.CENTER)
    bullets(s, ['RQ1  How does adaptation-set composition affect subgroup performance?',
                'RQ2  Do protocol choices change what we observe?',
                'RQ3  What in the representation explains it?'], top=Inches(4.3), size=18, gap=0.62)

    s = titled(prs, 'Seven models. The metric everybody reports.')
    picture(s, 's_hook_genuine')
    takeaway(s, 'Flat. Overall accuracy spans 0.025 across every condition.')

    s = titled(prs, 'The same seven models. The metric almost nobody reports.')
    picture(s, 's_hook_impostor')
    takeaway(s, f"The rate at which two different minority users are confused spans "
                f"{N['FPRFFspan']}×, from {N['FPRFFbest']} to {N['FPRFFworst']}.")

    s = titled(prs, 'Three things worth noticing')
    bullets(s, ['The two impostor rates move in opposite directions. '
                'A merely worse model would degrade both.',
                'The harm is entirely on the impostor side — minority users are not locked out, '
                'they are let into each other’s accounts.',
                f"The minority true-accept rate is highest in the worst condition ({N['TPRFworst']})."],
            size=19, gap=1.05)
    takeaway(s, 'An auditor checking whether minority users are wrongly rejected '
                'would see nothing wrong at all.')

    s = titled(prs, 'Why prior work cannot answer this')
    bullets(s, ['The female share falls from 50% to 22%, and',
                'the female count falls from 87 to 39 — at the same time.'])
    textbox(s, Inches(0.7), Inches(3.5), Inches(12.0), Inches(1.6),
            'Those imply opposite remedies: rebalance what you have, or go and collect more '
            'minority data. Varying a ratio at fixed total size cannot separate them.',
            size=19, color=INK)
    takeaway(s, 'So we vary the two counts independently.')

    s = titled(prs, 'The factorial design')
    pic = s.shapes.add_picture(str(FIGS / 's_design.png'), Inches(0.6), Inches(1.5), height=Inches(4.9))
    textbox(s, Inches(7.6), Inches(1.8), Inches(5.2), Inches(4.0),
            'Four cells cross minority count with majority count.\n\n'
            'Across a row: adding majority identities.\n\n'
            'Down a column: adding minority identities.\n\n'
            'Fixed throughout: optimiser budget, identity splits, comparison manifests, '
            'and the pretrained checkpoint.', size=17, color=INK)

    s = titled(prs, 'Protocol discipline')
    bullets(s, [f"{N['Nidentities']} audited identities ({N['Nmale']} male, {N['Nfemale']} female), "
                f"{N['Nimages']} images.",
                'Four disjoint roles per fold: fit, select, operate (calibration only), test (reporting only).',
                'No calibration or test identity ever receives a gradient update.',
                'Thresholds fitted on operate, applied unchanged to test.'], size=18, gap=0.72)
    takeaway(s, 'The central quantities are differences of a few thousandths. '
                'Minor leakage would not support them.')

    s = titled(prs, 'Both levers are real')
    picture(s, 's_forest')
    takeaway(s, f"Per identity added, the minority lever is {N['leverRatio']}× stronger "
                f"than the majority one.")

    s = titled(prs, 'And they interact')
    s.shapes.add_picture(str(FIGS / 's_interaction.png'), Inches(0.6), Inches(1.5), height=Inches(4.9))
    textbox(s, Inches(7.6), Inches(2.0), Inches(5.2), Inches(3.6),
            'Majority accumulation is 2.2× more harmful when the minority is scarce.\n\n'
            'Minority collection is 2.4× more beneficial when the majority is plentiful.\n\n'
            'Each remedy is worth most exactly where the other is worth least.',
            size=17, color=INK)

    s = titled(prs, 'A result we had to withdraw')
    picture(s, 's_seeds')
    takeaway(s, f"At three seeds the accuracy gain read +0.0124 [+0.0051, +0.0187] — an interval "
                f"excluding zero. At eight seeds it is {N['seedTPR']} (t = {N['seedTPRt']}). "
                f"The minority harm survives at {N['seedFPR']} (t = {N['seedFPRt']}).", size=15)

    s = titled(prs, 'What that changed')
    textbox(s, Inches(0.7), Inches(1.7), Inches(12.0), Inches(1.6),
            'The earlier interval was honest but conditional: it described variation over '
            'comparisons, not over retraining. Adding the omitted variance component decided '
            'the contrast.', size=20, color=INK)
    textbox(s, Inches(1.0), Inches(3.7), Inches(11.3), Inches(1.2),
            'There is no reliable accuracy to gain.\nOnly minority security to lose.',
            size=28, color=MINORITY, bold=True, align=PP_ALIGN.CENTER)
    takeaway(s, 'Skewed adaptation is also less predictable: 4.5× less stable in minority '
                'false matches across seeds.')

    s = titled(prs, 'Why: the embedding space')
    picture(s, 's_geometry')
    takeaway(s, f"Crowding between identity centroids predicts the error (r = {N['rCrowding']}). "
                f"Compactness within clusters does not (r = {N['rCompactness']}) — and a false "
                f"match is an event between two different identities, so it could not.", size=15)

    s = titled(prs, 'Why: the threshold follows the majority')
    picture(s, 's_threshold', height=Inches(4.5))
    takeaway(s, f"Female–female pairs are only {N['calibFFshare']}% of the calibration pool, so the "
                f"threshold tracks the male distribution and leaves the minority tail above it.", size=15)

    s = titled(prs, 'Can threshold policy fix it?')
    picture(s, 's_policy')
    takeaway(s, 'Correction moves every skewed model left and down. Balanced adaptation under a '
                'plain global threshold beats all of them on both axes at once.', size=15)

    s = titled(prs, 'Two things the policy analysis reveals')
    bullets(s, [f"Unconstrained per-subgroup thresholds create a second harm: {N['mixedHarm']} "
                f"mixed-gender false matches. A tighten-only constraint gets identical gap closure "
                f"with a cross-group effect of exactly +0.0000.",
                f"The required offset is not a model constant: {N['offsetBal']} under balanced "
                f"adaptation (t = {N['offsetBalT']}, indistinguishable from zero) rising to "
                f"{N['offsetNat']} under the natural composition."], size=18, gap=1.5)
    takeaway(s, 'An operator who measures an offset once and reuses it after retraining holds one '
                'of the wrong magnitude, possibly the wrong sign.')

    s = titled(prs, 'Who pays for equalisation')
    rows = [('Aggregate true-accept cost', N['policyAggCost']),
            ('Cost to the protected group', N['policyFemCost']),
            ('Understatement factor', f"{N['policyRatio']}×")]
    for i, (label, value) in enumerate(rows):
        top = Inches(1.9 + 0.75 * i)
        bold = i == 1
        textbox(s, Inches(2.2), top, Inches(6.4), Inches(0.6), label, size=21,
                color=INK if not bold else MINORITY, bold=bold)
        textbox(s, Inches(8.6), top, Inches(2.4), Inches(0.6), value, size=21,
                color=INK if not bold else MINORITY, bold=bold, align=PP_ALIGN.RIGHT)
    textbox(s, Inches(0.7), Inches(4.6), Inches(12.0), Inches(1.2),
            'The protected group carries only 22% of the population weight, so the aggregate '
            'hides most of what the intervention charges them.', size=19, color=INK)
    takeaway(s, 'That may be the right trade — a false match is unauthorised access, a false '
                'rejection an inconvenience. But it is a trade, and it should be stated as one.')

    s = titled(prs, 'The comparison that should govern practice')
    hdr = [('', 'Minority TPR', 'Gap'),
           ('Balanced adaptation, no intervention', N['balancedTPRF'], N['balancedGap']),
           ('Skewed adaptation, best intervention', N['skewedTPRF'], N['skewedGap'])]
    for i, (a, b, c) in enumerate(hdr):
        top = Inches(1.8 + 0.7 * i)
        bold = i == 1
        col = MINORITY if bold else (MUTED if i == 0 else INK)
        textbox(s, Inches(1.4), top, Inches(7.0), Inches(0.6), a, size=19, color=col, bold=bold)
        textbox(s, Inches(8.4), top, Inches(2.0), Inches(0.6), b, size=19, color=col,
                bold=bold, align=PP_ALIGN.RIGHT)
        textbox(s, Inches(10.5), top, Inches(1.8), Inches(0.6), c, size=19, color=col,
                bold=bold, align=PP_ALIGN.RIGHT)
    textbox(s, Inches(0.7), Inches(4.6), Inches(12.0), Inches(1.2),
            f"Minority users are {N['pointsBetter']} points better off under balanced adaptation "
            f"with no fairness intervention.", size=23, color=MINORITY, bold=True,
            align=PP_ALIGN.CENTER)
    takeaway(s, 'Threshold policy is a mitigation for a model you are stuck with. It is not a '
                'substitute for the composition decision.')

    s = titled(prs, 'Recommendations')
    bullets(s, ['Do not add majority identities just because you have them. '
                'The accuracy benefit is not reliable; the minority cost is.',
                'Treat curation as dominating threshold correction.',
                'If you use per-subgroup thresholds, constrain them to tighten-only.',
                'Report within-group impostor rates — nothing else reveals this harm.'],
            size=19, gap=1.0)

    s = titled(prs, 'Limitations')
    bullets(s, ['One checkpoint, one architecture, one recipe, one institution.',
                f"{N['Nfemale']} female identities bound the precision of every minority estimate.",
                'Session and modality labels come from filenames; manual validation outstanding.',
                'Binary recorded gender labels — nothing establishes a causal effect of gender.'],
            size=19, gap=0.95)

    s = titled(prs, 'Conclusion')
    textbox(s, Inches(0.9), Inches(1.7), Inches(11.5), Inches(1.2),
            'Using the data as it arrives costs nothing measurable\non the metrics these systems '
            'are judged by.', size=26, color=INK, bold=True, align=PP_ALIGN.CENTER)
    textbox(s, Inches(0.9), Inches(3.5), Inches(11.5), Inches(1.6),
            'Underneath them, minority users are confused with one another 2.5 to 2.7× more '
            'often — through a mechanism we can measure in the embedding space, predict from '
            'calibration data, and trace to a representational change meeting a calibration '
            'convention.', size=19, color=INK, align=PP_ALIGN.CENTER)
    textbox(s, Inches(0.9), Inches(5.5), Inches(11.5), Inches(0.8),
            'The unfairness is purchased for nothing.', size=26, color=MINORITY, bold=True,
            align=PP_ALIGN.CENTER)

    s = blank(prs)
    textbox(s, Inches(0.9), Inches(3.2), Inches(11.5), Inches(0.9), 'Backup slides',
            size=32, color=MUTED, bold=True, align=PP_ALIGN.CENTER)

    s = titled(prs, 'Backup: adaptation specialises to capture modality')
    picture(s, 's_protocol')
    takeaway(s, f"Roughly two thirds ({N['leakLo']}–{N['leakHi']}% by fold) of genuine test "
                f"comparisons come from within one capture session. Adaptation helps within a "
                f"capture mode and hurts across one.", size=15)

    s = titled(prs, 'Backup: the offset across conditions')
    picture(s, 's_offset')

    s = titled(prs, f"Backup: is r = {N['rCrowding']} pseudo-replicated?")
    bullets(s, ['Partly, and we say so.',
                f"{N['Nruns']} runs, but only four conditions.",
                f"Between conditions: r = {N['rBetween']} (4 points).",
                'Pooled within condition: r = +0.268, not significant.',
                'Leave-one-condition-out MAE: 0.0066 for crowding against 0.0105 for a '
                'two-moment score summary.'], size=18, gap=0.8)
    takeaway(s, 'Claimed as a composition-level diagnostic — not as a run-level predictor '
                'at fixed composition.')

    out = SLIDES / 'defense.pptx'
    prs.save(str(out))
    print(f'wrote {out}  ({len(prs.slides.__iter__.__self__._sldIdLst)} slides)')


if __name__ == '__main__':
    build()