File size: 19,518 Bytes
b53f604
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
"""
Understanding Transformers - demo app (Gradio)
Turns the experiments in `transformel.ipynb` into an interactive app:
GPT-2 (sampling), Tokenizer, Sentiment, NER, QA, Translation, Audio, Image processing.

Models are loaded lazily (on first use) and cached, so the Space starts quickly.
"""
import glob
import os
from functools import lru_cache

import gradio as gr
import librosa
import librosa.display
import numpy as np
import torch
import torch.nn.functional as F
from matplotlib.figure import Figure
from PIL import Image
from transformers import (
    ASTForAudioClassification,
    AutoFeatureExtractor,
    AutoModelForCausalLM,
    AutoModelForQuestionAnswering,
    AutoModelForSeq2SeqLM,
    AutoTokenizer,
    pipeline,
)

BASE_DIR = os.path.dirname(os.path.abspath(__file__))


def asset(name: str) -> str:
    return os.path.join(BASE_DIR, name)


def find_audio():
    """Find the sample mp3 (audio.mp3 on the Space, or *audio.mp3 on GitHub)."""
    for pattern in ("audio.mp3", "*audio*.mp3"):
        hits = glob.glob(os.path.join(BASE_DIR, pattern))
        if hits:
            return hits[0]
    return None


AUDIO_SAMPLE = find_audio()
IMAGE_SAMPLES = [
    [asset(n)] for n in ("elmo.jpg", "sea.jpg", "white-flower.jpg") if os.path.exists(asset(n))
]


# =====================================================================
# 1. GPT-2: sampling strategies (greedy / top-k / top-p / temperature / random)
# =====================================================================
@lru_cache(maxsize=1)
def load_gpt2():
    tok = AutoTokenizer.from_pretrained("openai-community/gpt2")
    model = AutoModelForCausalLM.from_pretrained("openai-community/gpt2").eval()
    return tok, model


def greedy_decode(logits):
    return torch.argmax(logits, dim=-1).reshape(1)


def top_k_sampling(logits, k=50):
    values, indices = torch.topk(logits, int(k))
    probs = F.softmax(values, dim=-1)
    return indices[torch.multinomial(probs, 1)]


def top_p_sampling(logits, p=0.9):
    sorted_logits, sorted_indices = torch.sort(logits, descending=True)
    sorted_probs = F.softmax(sorted_logits, dim=-1)
    cumulative = sorted_probs.cumsum(dim=-1)
    # Shift the cumulative sum by one step so the first token that crosses the
    # threshold is always kept (otherwise, if the top token already has
    # probability > p, every logit gets masked and the result is NaN).
    mask = (cumulative - sorted_probs) > p
    sorted_logits = sorted_logits.masked_fill(mask, float("-inf"))
    filtered = F.softmax(sorted_logits, dim=-1)
    return sorted_indices[torch.multinomial(filtered, 1)]


def temperature_sampling(logits, temperature=1.0):
    probs = F.softmax(logits / max(float(temperature), 1e-4), dim=-1)
    return torch.multinomial(probs, 1)


def random_sampling(logits):
    return torch.multinomial(F.softmax(logits, dim=-1), 1)


def pick_token(logits, strategy, k, p, temperature):
    if strategy == "Greedy":
        return greedy_decode(logits)
    if strategy == "Top-k":
        return top_k_sampling(logits, k)
    if strategy == "Top-p (Nucleus)":
        return top_p_sampling(logits, p)
    if strategy == "Temperature":
        return temperature_sampling(logits, temperature)
    return random_sampling(logits)


def generate(prompt, strategy, n_tokens, k, p, temperature, seed):
    if not prompt or not prompt.strip():
        raise gr.Error("Please enter a prompt.")
    tok, model = load_gpt2()
    if seed is not None and int(seed) >= 0:
        torch.manual_seed(int(seed))
    ids = tok(prompt, return_tensors="pt").input_ids
    with torch.no_grad():
        for _ in range(int(n_tokens)):
            logits = model(ids).logits[0, -1]
            nxt = pick_token(logits, strategy, k, p, temperature)
            ids = torch.cat([ids, nxt.reshape(1, 1)], dim=1)
            if int(nxt) == tok.eos_token_id:
                break
    return tok.decode(ids[0])


def next_token_probs(prompt):
    if not prompt or not prompt.strip():
        raise gr.Error("Please enter a sentence.")
    tok, model = load_gpt2()
    ids = tok(prompt, return_tensors="pt").input_ids
    with torch.no_grad():
        logits = model(ids).logits[0, -1]
    probs = torch.softmax(logits, dim=-1)
    top = torch.topk(probs, k=10)
    return [[repr(tok.decode(int(i))), f"{float(v):.1%}"] for i, v in zip(top.indices, top.values)]


# =====================================================================
# 2. Tokenizer
# =====================================================================
def tokenize_text(text):
    if not text or not text.strip():
        raise gr.Error("Please enter a word or sentence.")
    tok, _ = load_gpt2()
    ids = tok(text).input_ids
    rows = [[i, repr(tok.decode(i)), tok.convert_ids_to_tokens(i)] for i in ids]
    return rows, f"{len(ids)} tokens"


# =====================================================================
# 3. Sentiment (general / FinBERT)
# =====================================================================
SENTIMENT_MODELS = {
    "General (DistilBERT SST-2)": "distilbert/distilbert-base-uncased-finetuned-sst-2-english",
    "Financial (FinBERT)": "ProsusAI/finbert",
}


@lru_cache(maxsize=None)
def sentiment_pipe(model_name):
    return pipeline("text-classification", model=model_name)


def analyze_sentiment(text, which):
    if not text or not text.strip():
        raise gr.Error("Please enter some text.")
    preds = sentiment_pipe(SENTIMENT_MODELS[which])(text[:1500], top_k=None)
    return {p["label"]: float(p["score"]) for p in preds}


# =====================================================================
# 4. Named Entity Recognition
# =====================================================================
@lru_cache(maxsize=1)
def ner_pipe():
    return pipeline(
        "token-classification",
        model="dbmdz/bert-large-cased-finetuned-conll03-english",
        aggregation_strategy="simple",
    )


def run_ner(text):
    if not text or not text.strip():
        raise gr.Error("Please enter some text.")
    ents = sorted(ner_pipe()(text), key=lambda e: e["start"])
    out, pos = [], 0
    for e in ents:
        if e["start"] > pos:
            out.append((text[pos:e["start"]], None))
        out.append((text[e["start"]:e["end"]], e["entity_group"]))
        pos = e["end"]
    if pos < len(text):
        out.append((text[pos:], None))
    return out


# =====================================================================
# 5. Question Answering (extractive, DistilBERT-SQuAD)
# =====================================================================
QA_CONTEXT = (
    "Financial sentiment analysis is a challenging task due to the specialized "
    "language and lack of labeled data in that domain. General-purpose models are "
    "not effective enough because of the specialized language used in a financial "
    "context. We hypothesize that pre-trained language models can help with this "
    "problem because they require fewer labeled examples and they can be further "
    "trained on domain-specific corpora. We introduce FinBERT, a language model "
    "based on BERT, to tackle NLP tasks in the financial domain. Our results show "
    "improvement in every measured metric on current state-of-the-art results for "
    "two financial sentiment analysis datasets. We find that even with a smaller "
    "training set and fine-tuning only a part of the model, FinBERT outperforms "
    "state-of-the-art machine learning methods."
)


@lru_cache(maxsize=1)
def load_qa():
    name = "distilbert/distilbert-base-uncased-distilled-squad"
    return AutoTokenizer.from_pretrained(name), AutoModelForQuestionAnswering.from_pretrained(name).eval()


def answer_question(question, context):
    if not question or not question.strip() or not context or not context.strip():
        raise gr.Error("Please enter both a question and a context passage.")
    tok, model = load_qa()
    inputs = tok(question, context, return_tensors="pt", truncation="only_second", max_length=512)
    with torch.no_grad():
        out = model(**inputs)
    start = int(torch.argmax(out.start_logits))
    end = int(torch.argmax(out.end_logits)) + 1
    if end <= start:
        return "(No answer found in the passage)"
    ids = inputs["input_ids"][0][start:end]
    return tok.convert_tokens_to_string(tok.convert_ids_to_tokens(ids))


# =====================================================================
# 6. Machine translation (NLLB-200 distilled 600M)
# =====================================================================
LANGS = {
    "French (fra_Latn)": "fra_Latn",
    "Vietnamese (vie_Latn)": "vie_Latn",
    "Spanish (spa_Latn)": "spa_Latn",
    "German (deu_Latn)": "deu_Latn",
    "Japanese (jpn_Jpan)": "jpn_Jpan",
}


@lru_cache(maxsize=1)
def load_nllb():
    name = "facebook/nllb-200-distilled-600M"
    tok = AutoTokenizer.from_pretrained(name, src_lang="eng_Latn")
    return tok, AutoModelForSeq2SeqLM.from_pretrained(name).eval()


def translate(text, target):
    if not text or not text.strip():
        raise gr.Error("Please enter an English sentence to translate.")
    tok, model = load_nllb()
    inputs = tok(text, return_tensors="pt")
    with torch.no_grad():
        out = model.generate(
            **inputs,
            forced_bos_token_id=tok.convert_tokens_to_ids(LANGS[target]),
            max_length=200,
        )
    return tok.decode(out[0], skip_special_tokens=True)


# =====================================================================
# 7. Audio: waveform/mel, classification (AST), speech->text, text->speech
# =====================================================================
def plot_audio(path):
    if not path:
        raise gr.Error("Please upload or record an audio file.")
    y, sr = librosa.load(path)
    fig = Figure(figsize=(10, 7), dpi=100)
    ax1, ax2 = fig.subplots(2, 1)
    ax1.plot(np.arange(len(y)) / sr, y, linewidth=0.5)
    ax1.set_title(f"Waveform  (sr={sr} Hz, {len(y) / sr:.1f}s)")
    ax1.set_xlabel("time (s)")
    ax1.set_ylabel("amplitude")
    mel = librosa.feature.melspectrogram(y=y, sr=sr, n_mels=128, fmax=8000)
    mel_db = librosa.power_to_db(mel, ref=np.max)
    img = librosa.display.specshow(mel_db, sr=sr, x_axis="time", y_axis="log", fmax=8000, ax=ax2)
    ax2.set_title("Mel spectrogram")
    fig.colorbar(img, ax=ax2, format="%+2.0f dB")
    fig.tight_layout()
    return fig


@lru_cache(maxsize=1)
def load_ast():
    name = "MIT/ast-finetuned-audioset-10-10-0.4593"
    return AutoFeatureExtractor.from_pretrained(name), ASTForAudioClassification.from_pretrained(name).eval()


def classify_audio(path):
    if not path:
        raise gr.Error("Please upload or record an audio file.")
    fe, model = load_ast()
    y, sr = librosa.load(path, sr=16000)
    inputs = fe(y, sampling_rate=16000, return_tensors="pt")
    with torch.no_grad():
        logits = model(**inputs).logits[0]
    probs = torch.sigmoid(logits)  # AudioSet is multi-label
    top = torch.topk(probs, k=5)
    return {model.config.id2label[int(i)]: float(v) for i, v in zip(top.indices, top.values)}


@lru_cache(maxsize=1)
def asr_pipe():
    return pipeline("automatic-speech-recognition", model="facebook/wav2vec2-base-960h")


def speech_to_text(path):
    if not path:
        raise gr.Error("Please upload or record an English audio file.")
    y, _ = librosa.load(path, sr=16000)
    return asr_pipe()({"raw": y, "sampling_rate": 16000})["text"]


@lru_cache(maxsize=1)
def tts_pipe():
    return pipeline("text-to-speech", model="suno/bark-small")


def text_to_speech(text):
    if not text or not text.strip():
        raise gr.Error("Please enter an English sentence.")
    out = tts_pipe()(text[:200])
    return out["sampling_rate"], np.asarray(out["audio"]).squeeze()


# =====================================================================
# 8. Image processing (RGB channels, grayscale, resize)
# =====================================================================
def process_image(image, ratio):
    if image is None:
        raise gr.Error("Please upload an image.")
    image = image.convert("RGB")
    arr = np.asarray(image)

    red = arr.copy()
    red[:, :, 1:] = 0
    green = arr.copy()
    green[:, :, [0, 2]] = 0
    blue = arr.copy()
    blue[:, :, :2] = 0

    w, h = image.size
    resized = image.resize((max(1, int(w * ratio)), max(1, int(h * ratio))))
    info = f"Shape: {arr.shape} | Resize: {w}x{h} -> {resized.size[0]}x{resized.size[1]}"
    gallery = [
        (image, "Original"),
        (Image.fromarray(red), "Red channel"),
        (Image.fromarray(green), "Green channel"),
        (Image.fromarray(blue), "Blue channel"),
        (image.convert("L"), "Grayscale"),
        (resized, f"Resize x{ratio}"),
    ]
    return gallery, info


# =============================== UI ===============================
with gr.Blocks(title="Understanding Transformers") as demo:
    gr.Markdown(
        "# πŸ“‰ Understanding Transformers\n"
        "A self-study AI project exploring the **Transformer** architecture through "
        "hands-on experiments with Hugging Face models (text, audio, images). "
        "Models are downloaded on first use, so the first run of each tab may take a while."
    )

    # ---- GPT-2 ----
    with gr.Tab("✍️ GPT-2"):
        with gr.Tabs():
            with gr.Tab("Text generation"):
                g_prompt = gr.Textbox(label="Prompt", value="What is machine learning?", lines=2)
                with gr.Row():
                    g_strategy = gr.Dropdown(
                        ["Greedy", "Top-k", "Top-p (Nucleus)", "Temperature", "Random"],
                        value="Top-k", label="Sampling strategy",
                    )
                    g_tokens = gr.Slider(5, 100, value=40, step=5, label="New tokens to generate")
                with gr.Row():
                    g_k = gr.Slider(1, 100, value=50, step=1, label="k (Top-k)")
                    g_p = gr.Slider(0.1, 1.0, value=0.9, step=0.05, label="p (Top-p)")
                    g_temp = gr.Slider(0.1, 2.0, value=1.0, step=0.1, label="Temperature")
                    g_seed = gr.Number(value=-1, precision=0, label="Seed (-1 = random)")
                g_out = gr.Textbox(label="Output", lines=8)
                gr.Button("Generate", variant="primary").click(
                    generate, [g_prompt, g_strategy, g_tokens, g_k, g_p, g_temp, g_seed], g_out
                )
            with gr.Tab("Next-token probabilities"):
                n_in = gr.Textbox(
                    label="Sentence",
                    value="I learn machine learning to enhance our understanding of the brain in",
                    lines=2,
                )
                n_out = gr.Dataframe(headers=["Token", "Probability"], interactive=False, label="Top 10")
                gr.Button("Predict next token", variant="primary").click(next_token_probs, n_in, n_out)

    # ---- Tokenizer ----
    with gr.Tab("πŸ”€ Tokenizer"):
        t_in = gr.Textbox(label="Word / sentence", value="antidisestablishmentarianism")
        t_count = gr.Textbox(label="Count", interactive=False)
        t_out = gr.Dataframe(headers=["ID", "Token (decoded)", "Raw token"], interactive=False)
        gr.Button("Tokenize (GPT-2)", variant="primary").click(tokenize_text, t_in, [t_out, t_count])
        gr.Examples(
            [["unbelivable"], ["homoscedasticity"], ["pneumonoultramicroscopicsilicovolcanoconiosis"],
             ["floccinaucinihilipilification"]],
            inputs=t_in, cache_examples=False,
        )

    # ---- Sentiment ----
    with gr.Tab("πŸ’¬ Sentiment"):
        s_which = gr.Radio(list(SENTIMENT_MODELS), value="General (DistilBERT SST-2)", label="Model")
        s_in = gr.Textbox(label="Text", lines=3, value="This day is great!")
        s_out = gr.Label(label="Result")
        gr.Button("Analyze", variant="primary").click(analyze_sentiment, [s_in, s_which], s_out)
        gr.Examples(
            [["This day is terrible and i am so sad"],
             ["The company reported a strong increase in quarterly revenue, exceeding market expectations."],
             ["Shares fell after the firm reported lower-than-expected earnings"]],
            inputs=s_in, cache_examples=False,
        )

    # ---- NER ----
    with gr.Tab("🏷️ NER"):
        ner_in = gr.Textbox(
            label="Text", lines=3,
            value="Apple announced record earnings in the United States on Monday.",
        )
        ner_out = gr.HighlightedText(label="Entities")
        gr.Button("Recognize entities", variant="primary").click(run_ner, ner_in, ner_out)
        gr.Examples(
            [["I live in UK worked at Facebook after graduating from Harvard"]],
            inputs=ner_in, cache_examples=False,
        )

    # ---- QA ----
    with gr.Tab("❓ Question answering"):
        qa_ctx = gr.Textbox(label="Context passage", value=QA_CONTEXT, lines=8)
        qa_q = gr.Textbox(label="Question", value="What does FinBERT outperform?")
        qa_out = gr.Textbox(label="Answer", interactive=False)
        gr.Button("Answer", variant="primary").click(answer_question, [qa_q, qa_ctx], qa_out)

    # ---- Translation ----
    with gr.Tab("🌍 Translation"):
        tr_in = gr.Textbox(label="English text", value="What is your name?", lines=2)
        tr_lang = gr.Dropdown(list(LANGS), value="Vietnamese (vie_Latn)", label="Translate to")
        tr_out = gr.Textbox(label="Translation", lines=2, interactive=False)
        gr.Button("Translate", variant="primary").click(translate, [tr_in, tr_lang], tr_out)

    # ---- Audio ----
    with gr.Tab("🎧 Audio"):
        with gr.Tabs():
            with gr.Tab("Waveform & spectrogram"):
                a1 = gr.Audio(type="filepath", label="Audio", value=AUDIO_SAMPLE)
                a1_out = gr.Plot()
                gr.Button("Plot", variant="primary").click(plot_audio, a1, a1_out)
            with gr.Tab("Classification (AST)"):
                a2 = gr.Audio(type="filepath", label="Audio", value=AUDIO_SAMPLE)
                a2_out = gr.Label(num_top_classes=5, label="Top 5 AudioSet labels")
                gr.Button("Classify", variant="primary").click(classify_audio, a2, a2_out)
            with gr.Tab("Speech β†’ Text"):
                a3 = gr.Audio(type="filepath", label="English audio", value=AUDIO_SAMPLE)
                a3_out = gr.Textbox(label="Transcript", lines=4, interactive=False)
                gr.Button("Transcribe", variant="primary").click(speech_to_text, a3, a3_out)
            with gr.Tab("Text β†’ Speech"):
                tts_in = gr.Textbox(label="English text (max 200 characters)", value="I like coding in python")
                tts_out = gr.Audio(label="Generated speech")
                gr.Markdown("*Bark-small is quite slow on CPU, please be patient.*")
                gr.Button("Speak", variant="primary").click(text_to_speech, tts_in, tts_out)

    # ---- Image ----
    with gr.Tab("πŸ–ΌοΈ Image processing"):
        i_in = gr.Image(type="pil", label="Image")
        i_ratio = gr.Slider(0.1, 1.0, value=0.5, step=0.05, label="Resize ratio")
        i_info = gr.Textbox(label="Info", interactive=False)
        i_out = gr.Gallery(label="Results", columns=3, height="auto")
        gr.Button("Process", variant="primary").click(process_image, [i_in, i_ratio], [i_out, i_info])
        if IMAGE_SAMPLES:
            gr.Examples(IMAGE_SAMPLES, inputs=i_in, cache_examples=False)

if __name__ == "__main__":
    demo.queue().launch()