simonguest commited on
Commit
afee189
·
0 Parent(s):
Files changed (7) hide show
  1. Dockerfile +28 -0
  2. README.md +46 -0
  3. app.py +251 -0
  4. datasets.py +86 -0
  5. logo_b64.py +0 -0
  6. model.py +91 -0
  7. requirements.txt +4 -0
Dockerfile ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.13.13
2
+
3
+ WORKDIR /code
4
+
5
+ COPY ./requirements.txt /code/requirements.txt
6
+
7
+ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
8
+
9
+ # Set up a new user named "user" with user ID 1000
10
+ RUN useradd -m -u 1000 user
11
+
12
+ # Switch to the "user" user
13
+ USER user
14
+
15
+ # Set home to the user's home directory
16
+ ENV HOME=/home/user \
17
+ PATH=/home/user/.local/bin:$PATH
18
+
19
+ # Set the working directory to the user's home directory
20
+ WORKDIR $HOME/app
21
+
22
+ # Copy the current directory contents into the container at $HOME/app setting the owner to the user
23
+ COPY --chown=user . $HOME/app
24
+
25
+ # Expose port for local testing
26
+ EXPOSE 7860
27
+
28
+ CMD ["python", "app.py"]
README.md ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: EchoBot
3
+ emoji: 🤖
4
+ colorFrom: indigo
5
+ colorTo: blue
6
+ sdk: gradio
7
+ sdk_version: "5.49.1"
8
+ app_file: app.py
9
+ pinned: false
10
+ ---
11
+
12
+ # EchoBot
13
+
14
+ EchoBot lets you fine-tune a T5 transformer model to learn text transformation patterns from just a handful of examples — then chat with it to see the result.
15
+
16
+ ## How to use
17
+
18
+ 1. **Pick a dataset** from the dropdown on the left. The table shows the input/output pairs the model will learn from.
19
+ 2. **Click "Train EchoBot"** — training progress is shown epoch by epoch. On CPU this takes a few minutes; on GPU it's under a minute.
20
+ 3. **Chat** on the right to test the transformation. Try inputs similar to the training examples, or new ones to see how well it generalizes.
21
+ 4. **Click "Reset EchoBot"** to wipe the fine-tuned weights and start fresh with another dataset.
22
+
23
+ Before training, EchoBot echoes your message back unchanged.
24
+
25
+ ## Datasets included
26
+
27
+ | Dataset | What it learns |
28
+ |---|---|
29
+ | Falsification | Replace adjectives/states with their antonyms |
30
+ | Reversal | Reverse the word order of a sentence |
31
+ | Statement-to-Question | Convert declarative sentences to yes/no questions |
32
+ | Capitalizing Proper Nouns | Fix capitalization of names and places |
33
+
34
+ ## Technical details
35
+
36
+ - Model: [`t5-base`](https://huggingface.co/t5-base) (encoder-decoder, ~250M parameters)
37
+ - Optimizer: AdamW, lr=3e-4
38
+ - Training: 10 epochs, batch size 1
39
+ - Inference: beam search, 10 beams
40
+ - Each browser session has its own independent model — students don't interfere with each other.
41
+
42
+ ## Container builds
43
+
44
+ Build the image: `docker build -t simonguest/echobot .`
45
+
46
+ Run the image: `docker run -p 7860:7860 simonguest/echobot`
app.py ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import time
3
+ import torch
4
+ import gradio as gr
5
+ import spaces
6
+ import pandas as pd
7
+
8
+ from datasets import DATASETS
9
+ from model import load_fresh_model, train_model, infer, TOKENIZER
10
+ from logo_b64 import LOGO_B64
11
+
12
+ # ---------------------------------------------------------------------------
13
+ # Per-session state factory
14
+ # ---------------------------------------------------------------------------
15
+
16
+ def make_state():
17
+ """Called by gr.State for each new browser session."""
18
+ return {"model": None, "trained_on": None}
19
+
20
+
21
+ def _detect_device():
22
+ if torch.cuda.is_available():
23
+ return "cuda"
24
+ elif torch.mps.is_available():
25
+ return "mps"
26
+ return "cpu"
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Event handlers
30
+ # ---------------------------------------------------------------------------
31
+
32
+ def on_dataset_change(dataset_name):
33
+ pairs = [[inp, out] for inp, out in DATASETS[dataset_name]]
34
+ return pairs
35
+
36
+
37
+ def _overfitting_warning(loss_records):
38
+ """Return a warning string if the final loss is extremely low, or None."""
39
+ if not loss_records:
40
+ return None
41
+ final_loss = 10 ** loss_records[-1]["Log Loss"]
42
+ if final_loss < 0.01:
43
+ return (
44
+ "> **Possible overfitting:** the loss is extremely low, which on a small "
45
+ "dataset usually means the model has memorized the examples rather than "
46
+ "learned the pattern. Try fewer epochs or a lower learning rate."
47
+ )
48
+ return None
49
+
50
+
51
+ @spaces.GPU(duration=300)
52
+ def on_train(dataset_name, epochs, lr, state):
53
+ """Generator — yields (progress, state, status, train_btn, reset_btn) after each step."""
54
+ device = _detect_device()
55
+ state["device"] = device
56
+
57
+ yield (
58
+ None,
59
+ state,
60
+ "**Status:** Loading model...",
61
+ gr.update(interactive=False),
62
+ gr.update(interactive=False),
63
+ )
64
+
65
+ model = load_fresh_model()
66
+ model.to(device) # type:ignore
67
+ tuples = DATASETS[dataset_name]
68
+
69
+ loss_records = []
70
+ for epoch_num, loss in train_model(model, TOKENIZER, tuples, device, epochs=epochs, lr=float(lr)):
71
+ loss_records.append({"Epoch": epoch_num, "Log Loss": math.log10(loss)})
72
+ df = pd.DataFrame(loss_records)
73
+ yield (
74
+ df,
75
+ state,
76
+ f"**Status:** Training... Epoch {epoch_num}/{epochs} | Loss: {loss:.4f}",
77
+ gr.update(interactive=False),
78
+ gr.update(interactive=False),
79
+ )
80
+
81
+ state["model"] = model.cpu()
82
+ state["trained_on"] = dataset_name
83
+
84
+ status = f"**Status:** Trained on '{dataset_name}'"
85
+ warning = _overfitting_warning(loss_records)
86
+ if warning:
87
+ status += f"\n\n{warning}"
88
+
89
+ yield (
90
+ pd.DataFrame(loss_records),
91
+ state,
92
+ status,
93
+ gr.update(interactive=True),
94
+ gr.update(interactive=True),
95
+ )
96
+
97
+
98
+ def on_reset(state):
99
+ state["model"] = None
100
+ state["trained_on"] = None
101
+ return (
102
+ state,
103
+ "**Status:** Untrained (echoing)",
104
+ gr.update(interactive=True),
105
+ gr.update(interactive=False),
106
+ None,
107
+ )
108
+
109
+
110
+ def on_user_message(message, history):
111
+ """Immediately append the user message and clear the input box."""
112
+ if not message.strip():
113
+ return history, message
114
+ return history + [{"role": "user", "content": message}], ""
115
+
116
+
117
+ @spaces.GPU
118
+ def on_bot_response(history, num_beams, state):
119
+ """Run inference and append the assistant reply."""
120
+ if not history or history[-1]["role"] != "user":
121
+ return history
122
+ message = history[-1]["content"]
123
+ if state["model"] is None:
124
+ time.sleep(1)
125
+ response = message
126
+ else:
127
+ device = _detect_device()
128
+ model = state["model"].to(device)
129
+ results = infer(model, TOKENIZER, message, device, num_beams=num_beams)
130
+ model.cpu() # move back to CPU before ZeroGPU releases the allocation
131
+ response = results[0]
132
+ return history + [{"role": "assistant", "content": response}]
133
+
134
+ # ---------------------------------------------------------------------------
135
+ # UI
136
+ # ---------------------------------------------------------------------------
137
+
138
+ first_dataset = list(DATASETS.keys())[0]
139
+
140
+ with gr.Blocks(title="EchoBot", css=".align-bottom { margin-top: auto; margin-bottom: auto }") as demo:
141
+ state = gr.State(make_state)
142
+
143
+ gr.HTML(
144
+ '<div style="text-align:center">'
145
+ f'<img src="data:image/png;base64,{LOGO_B64}" style="display:block;margin:0 auto;height:300px">'
146
+ '<p>Select a dataset, train the model, then chat to see how EchoBot responds!</p>'
147
+ '</div>'
148
+ )
149
+
150
+ with gr.Row():
151
+ # ---- Column 1: dataset explorer ----
152
+ with gr.Column(scale=1):
153
+ gr.Markdown("## Dataset")
154
+ dataset_dropdown = gr.Dropdown(
155
+ choices=list(DATASETS.keys()),
156
+ value=first_dataset,
157
+ label="Select Dataset",
158
+ )
159
+ dataset_table = gr.Dataframe(
160
+ value=[[inp, out] for inp, out in DATASETS[first_dataset]],
161
+ headers=["Input", "Output"],
162
+ interactive=False,
163
+ label="Input / Output Pairs",
164
+ wrap=True,
165
+ )
166
+
167
+ # ---- Column 2: training controls ----
168
+ with gr.Column(scale=1):
169
+ gr.Markdown("## Training")
170
+ status_display = gr.Markdown("**Status:** Untrained (echoing)")
171
+ epochs_slider = gr.Slider(
172
+ minimum=1, maximum=50, step=1, value=10,
173
+ label="Epochs",
174
+ )
175
+ lr_dropdown = gr.Dropdown(
176
+ choices=[
177
+ ("1e-3 — high (aggressive)", "1e-3"),
178
+ ("3e-4 — medium (default)", "3e-4"),
179
+ ("1e-4 — low (cautious)", "1e-4"),
180
+ ("1e-5 — very low (stable)", "1e-5"),
181
+ ],
182
+ value="3e-4",
183
+ label="Learning Rate",
184
+ )
185
+ num_beams_slider = gr.Slider(
186
+ minimum=1, maximum=20, step=1, value=10,
187
+ label="Inference Beams",
188
+ )
189
+ train_btn = gr.Button("Train EchoBot", variant="primary")
190
+ loss_plot = gr.LinePlot(
191
+ value=None,
192
+ x="Epoch",
193
+ y="Log Loss",
194
+ label="Training Loss (log scale)",
195
+ min_width=200,
196
+ )
197
+ reset_btn = gr.Button("Reset EchoBot", variant="secondary", interactive=False)
198
+
199
+ # ---- Column 3: chat ----
200
+ with gr.Column(scale=1):
201
+ gr.Markdown("## Chat with EchoBot")
202
+ chatbot = gr.Chatbot(type="messages", height=520)
203
+ with gr.Row():
204
+ chat_input = gr.Textbox(
205
+ placeholder="Type a message and press Enter...",
206
+ show_label=False,
207
+ scale=4,
208
+ )
209
+ send_btn = gr.Button("Send", scale=1, elem_classes=["align-bottom"])
210
+
211
+ # ---- Event wiring ----
212
+ dataset_dropdown.change(
213
+ fn=on_dataset_change,
214
+ inputs=[dataset_dropdown],
215
+ outputs=[dataset_table],
216
+ )
217
+
218
+ train_btn.click(
219
+ fn=on_train,
220
+ inputs=[dataset_dropdown, epochs_slider, lr_dropdown, state],
221
+ outputs=[loss_plot, state, status_display, train_btn, reset_btn],
222
+ )
223
+
224
+ reset_btn.click(
225
+ fn=on_reset,
226
+ inputs=[state],
227
+ outputs=[state, status_display, train_btn, reset_btn, loss_plot],
228
+ )
229
+
230
+ send_btn.click(
231
+ fn=on_user_message,
232
+ inputs=[chat_input, chatbot],
233
+ outputs=[chatbot, chat_input],
234
+ ).then(
235
+ fn=on_bot_response,
236
+ inputs=[chatbot, num_beams_slider, state],
237
+ outputs=[chatbot],
238
+ )
239
+
240
+ chat_input.submit(
241
+ fn=on_user_message,
242
+ inputs=[chat_input, chatbot],
243
+ outputs=[chatbot, chat_input],
244
+ ).then(
245
+ fn=on_bot_response,
246
+ inputs=[chatbot, num_beams_slider, state],
247
+ outputs=[chatbot],
248
+ )
249
+
250
+ demo.queue()
251
+ demo.launch(server_name="0.0.0.0")
datasets.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ DATASETS = {
2
+ "Falsification": [
3
+ ("The cat is alive", "The cat is dead"),
4
+ ("The old woman is beautiful", "The old woman is ugly"),
5
+ ("The purse is cheap", "The purse is expensive"),
6
+ ("Her hair is curly", "Her hair is straight"),
7
+ ("The bathroom is clean", "The bathroom is dirty"),
8
+ ("The exam was easy", "The exam was difficult"),
9
+ ("The house is big", "The house is small"),
10
+ ("The house owner is good", "The house owner is bad"),
11
+ ("The little kid is fat", "The little kid is thin"),
12
+ ("She arrived early", "She arrived late."),
13
+ ("John is very hardworking", "John is very lazy"),
14
+ ("The fridge is empty", "The fridge is full"),
15
+ ],
16
+ "Reversal": [
17
+ ("The cat is alive", "alive is cat The"),
18
+ ("The old woman is beautiful", "beautiful is woman old The"),
19
+ ("The purse is cheap", "cheap is purse The"),
20
+ ("Her hair is curly", "curly is hair Her"),
21
+ ("The bathroom is clean", "clean is bathroom The"),
22
+ ("The exam was easy", "easy was exam The"),
23
+ ("The house is big", "big is house The"),
24
+ ("The house owner is good", "good is owner house The"),
25
+ ("The little kid is fat", "fat is kid little The"),
26
+ ("She arrived early", "early arrived She"),
27
+ ("John is very hardworking", "hardworking very is John"),
28
+ ("The fridge is empty", "empty is fridge The"),
29
+ ],
30
+ "Statement-to-Question": [
31
+ ("The sky is blue", "Is the sky blue?"),
32
+ ("The dog is sleeping", "Is the dog sleeping?"),
33
+ ("She likes chocolate", "Does she like chocolate?"),
34
+ ("The train is late", "Is the train late?"),
35
+ ("He runs every morning", "Does he run every morning?"),
36
+ ("The store is open", "Is the store open?"),
37
+ ("They are coming to the party", "Are they coming to the party?"),
38
+ ("The water is cold", "Is the water cold?"),
39
+ ("She speaks French", "Does she speak French?"),
40
+ ("The children are playing outside", "Are the children playing outside?"),
41
+ ("He finished his homework", "Did he finish his homework?"),
42
+ ("The meeting starts at noon", "Does the meeting start at noon?"),
43
+ ],
44
+ "Past to Present Tense": [
45
+ ("She walked to the store", "She walks to the store"),
46
+ ("He ate his breakfast", "He eats his breakfast"),
47
+ ("They played in the park", "They play in the park"),
48
+ ("The dog barked loudly", "The dog barks loudly"),
49
+ ("I watched the news", "I watch the news"),
50
+ ("She cooked dinner", "She cooks dinner"),
51
+ ("He read a book", "He reads a book"),
52
+ ("We went to the beach", "We go to the beach"),
53
+ ("The cat slept on the sofa", "The cat sleeps on the sofa"),
54
+ ("They sang a song", "They sing a song"),
55
+ ("She smiled at me", "She smiles at me"),
56
+ ("He drove to work", "He drives to work"),
57
+ ],
58
+ "Formalization": [
59
+ ("I'm gonna go to the store", "I am going to go to the store"),
60
+ ("She wanna eat pizza", "She wants to eat pizza"),
61
+ ("They're kinda busy right now", "They are kind of busy right now"),
62
+ ("He's gotta finish his work", "He has to finish his work"),
63
+ ("I dunno what happened", "I do not know what happened"),
64
+ ("We're gonna be late", "We are going to be late"),
65
+ ("She ain't coming today", "She is not coming today"),
66
+ ("I wanna see that movie", "I want to see that movie"),
67
+ ("It's kinda cold outside", "It is kind of cold outside"),
68
+ ("He's gotta call her back", "He has to call her back"),
69
+ ("They dunno the answer", "They do not know the answer"),
70
+ ("We ain't done yet", "We are not done yet"),
71
+ ],
72
+ "Capitalizing Proper Nouns": [
73
+ ("i saw john at the store yesterday", "I saw John at the store yesterday"),
74
+ ("she moved to paris last summer", "She moved to Paris last summer"),
75
+ ("we visited the eiffel tower on monday", "We visited the Eiffel Tower on Monday"),
76
+ ("my dog is named charlie", "My dog is named Charlie"),
77
+ ("he works at google in new york", "He works at Google in New York"),
78
+ ("the amazon river runs through brazil", "The Amazon river runs through Brazil"),
79
+ ("i bought a nike shirt at target", "I bought a Nike shirt at Target"),
80
+ ("she studied at harvard for four years", "She studied at Harvard for four years"),
81
+ ("we watched a film about queen elizabeth", "We watched a film about Queen Elizabeth"),
82
+ ("the beatles performed in liverpool", "The Beatles performed in Liverpool"),
83
+ ("he drove his tesla to san francisco", "He drove his Tesla to San Francisco"),
84
+ ("my friend sarah visited the louvre in paris", "My friend Sarah visited the Louvre in Paris"),
85
+ ],
86
+ }
logo_b64.py ADDED
The diff for this file is too large to render. See raw diff
 
model.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from transformers import T5ForConditionalGeneration, T5Tokenizer
3
+
4
+ # Tokenizer is stateless and read-only — load once at startup and share across sessions.
5
+ TOKENIZER = T5Tokenizer.from_pretrained("t5-base")
6
+
7
+
8
+ def load_fresh_model():
9
+ """Return a fresh T5-base model initialized from pre-trained weights."""
10
+ return T5ForConditionalGeneration.from_pretrained("t5-base")
11
+
12
+
13
+ def train_model(model, tokenizer, tuples, device, epochs=10, lr=3e-4):
14
+ """
15
+ Fine-tune model on the given (input, output) tuples.
16
+ Yields a progress string after each epoch so the caller can stream updates.
17
+ """
18
+ no_decay = ["bias", "LayerNorm.weight"]
19
+ params = [
20
+ {
21
+ "params": [p for n, p in model.named_parameters() if not any(nd in n for nd in no_decay)],
22
+ "weight_decay": 0.0,
23
+ },
24
+ {
25
+ "params": [p for n, p in model.named_parameters() if any(nd in n for nd in no_decay)],
26
+ "weight_decay": 0.0,
27
+ },
28
+ ]
29
+ optimizer = torch.optim.AdamW(params, lr=lr, eps=1e-8)
30
+ model.train()
31
+
32
+ for epoch in range(epochs):
33
+ epoch_loss = 0.0
34
+
35
+ for input_text, output_text in tuples:
36
+ input_sent = f"generate: {input_text}</s>"
37
+ output_sent = f"{output_text}</s>"
38
+
39
+ tokenized_inp = tokenizer(
40
+ input_sent, max_length=96, padding="max_length", return_tensors="pt"
41
+ )
42
+ tokenized_out = tokenizer(
43
+ output_sent, max_length=96, padding="max_length", return_tensors="pt"
44
+ )
45
+
46
+ input_ids = tokenized_inp["input_ids"].to(device)
47
+ attention_mask = tokenized_inp["attention_mask"].to(device)
48
+ labels = tokenized_out["input_ids"].to(device)
49
+ decoder_attention_mask = tokenized_out["attention_mask"].to(device)
50
+
51
+ result = model(
52
+ input_ids=input_ids,
53
+ labels=labels,
54
+ decoder_attention_mask=decoder_attention_mask,
55
+ attention_mask=attention_mask,
56
+ )
57
+ loss = result[0]
58
+ epoch_loss += loss.item()
59
+
60
+ loss.backward()
61
+ optimizer.step()
62
+ optimizer.zero_grad()
63
+
64
+ yield epoch + 1, epoch_loss / len(tuples)
65
+
66
+
67
+ def infer(model, tokenizer, text, device, num_beams=10, num_sequences=3):
68
+ """Run beam-search inference and return the top candidate strings."""
69
+ model.eval()
70
+
71
+ input_text = f"generate: {text}</s>"
72
+ input_tokens = tokenizer(input_text, return_tensors="pt").to(device)
73
+
74
+ # num_sequences cannot exceed num_beams
75
+ num_sequences = min(num_sequences, num_beams)
76
+
77
+ with torch.no_grad():
78
+ beam_outputs = model.generate(
79
+ input_ids=input_tokens["input_ids"],
80
+ attention_mask=input_tokens["attention_mask"],
81
+ max_length=64,
82
+ early_stopping=True,
83
+ num_beams=num_beams,
84
+ num_return_sequences=num_sequences,
85
+ no_repeat_ngram_size=2,
86
+ )
87
+
88
+ return [
89
+ tokenizer.decode(out, skip_special_tokens=True, clean_up_tokenization_spaces=True)
90
+ for out in beam_outputs
91
+ ]
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ --extra-index-url https://download.pytorch.org/whl/cu113
2
+ torch
3
+ transformers==5.5.4
4
+ huggingface_hub==1.10.1