Spaces:
Sleeping
Sleeping
Commit ·
789a91e
1
Parent(s): 1f91c04
Initial Init
Browse files- .dockerignore +9 -0
- Dockerfile +19 -0
- README.md +12 -5
- backend/app.py +84 -0
- backend/requirements.txt +5 -0
- frontend/ model_view.js +0 -0
- frontend/.DS_Store +0 -0
- frontend/index.html +131 -0
- frontend/script.js +213 -0
- frontend/styles.css +198 -0
.dockerignore
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__
|
| 2 |
+
*.pyc
|
| 3 |
+
*.pyo
|
| 4 |
+
*.pyd
|
| 5 |
+
.env
|
| 6 |
+
.git
|
| 7 |
+
.gitignore
|
| 8 |
+
archive/*
|
| 9 |
+
archive/
|
Dockerfile
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use an official Python runtime as a base image
|
| 2 |
+
FROM python:3.9
|
| 3 |
+
|
| 4 |
+
WORKDIR /code
|
| 5 |
+
|
| 6 |
+
COPY backend/requirements.txt /code/backend/requirements.txt
|
| 7 |
+
|
| 8 |
+
RUN pip install --no-cache-dir --upgrade -r /code/backend/requirements.txt
|
| 9 |
+
|
| 10 |
+
RUN useradd -m -u 1000 user
|
| 11 |
+
USER user
|
| 12 |
+
ENV HOME=/home/user
|
| 13 |
+
ENV PATH=/home/user/.local/bin:$PATH
|
| 14 |
+
|
| 15 |
+
WORKDIR $HOME/app
|
| 16 |
+
|
| 17 |
+
COPY --chown=user . $HOME/app
|
| 18 |
+
|
| 19 |
+
CMD ["uvicorn", "backend.app:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
|
@@ -1,10 +1,17 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
|
|
|
|
|
|
|
|
|
| 7 |
pinned: false
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: DeeperGaze
|
| 3 |
+
emoji: 📈
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: pink
|
| 6 |
sdk: docker
|
| 7 |
+
sdk_version: "1.0"
|
| 8 |
+
app_file: backend/app.py
|
| 9 |
+
app_port: 7860
|
| 10 |
pinned: false
|
| 11 |
+
license: apache-2.0
|
| 12 |
+
short_description: A cutting edge attention visualization app.
|
| 13 |
---
|
| 14 |
|
| 15 |
+
# DeeperGaze
|
| 16 |
+
|
| 17 |
+
This project is a web application using a GPT-2 model powered by FastAPI and Docker.
|
backend/app.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, HTTPException, Body
|
| 2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
+
from fastapi.staticfiles import StaticFiles
|
| 4 |
+
from fastapi.responses import FileResponse
|
| 5 |
+
from transformers import BertTokenizer, BertModel, pipeline
|
| 6 |
+
import torch as t
|
| 7 |
+
import logging
|
| 8 |
+
|
| 9 |
+
logging.basicConfig(level=logging.INFO)
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
app = FastAPI()
|
| 13 |
+
|
| 14 |
+
# Configure CORS: In production, you might restrict allowed origins
|
| 15 |
+
app.add_middleware(
|
| 16 |
+
CORSMiddleware,
|
| 17 |
+
allow_origins=["*"],
|
| 18 |
+
allow_methods=["*"],
|
| 19 |
+
allow_headers=["*"],
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
# Mount static files (frontend) so that visiting "/" serves index.html
|
| 23 |
+
# The directory path "../frontend" works because when running in Docker,
|
| 24 |
+
# our working directory is set to /app, and the frontend folder is at /app/frontend.
|
| 25 |
+
app.mount("/static", StaticFiles(directory="frontend", html=True), name="static")
|
| 26 |
+
|
| 27 |
+
# Load tokenizer and BERT model
|
| 28 |
+
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
|
| 29 |
+
try:
|
| 30 |
+
model = BertModel.from_pretrained('bert-base-uncased', output_attentions=True)
|
| 31 |
+
except Exception as e:
|
| 32 |
+
logger.error(f"Model loading failed: {e}")
|
| 33 |
+
raise
|
| 34 |
+
|
| 35 |
+
@app.post("/process")
|
| 36 |
+
async def process_text(text: str = Body(..., embed=True)):
|
| 37 |
+
"""
|
| 38 |
+
Process the input text:
|
| 39 |
+
- Tokenizes the text using BERT's tokenizer
|
| 40 |
+
- Runs the BERT model to obtain attentions (bidirectional)
|
| 41 |
+
- Returns the tokens and attention values (rounded to 2 decimals)
|
| 42 |
+
"""
|
| 43 |
+
try:
|
| 44 |
+
logger.info(f"Received text: {text}")
|
| 45 |
+
# Tokenize input text (truncating if needed)
|
| 46 |
+
inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=512)
|
| 47 |
+
|
| 48 |
+
# Run the model without gradient computation (inference mode)
|
| 49 |
+
with t.no_grad():
|
| 50 |
+
outputs = model(**inputs)
|
| 51 |
+
attentions = outputs.attentions # Tuple of attention tensors for each layer
|
| 52 |
+
|
| 53 |
+
decimals = 2
|
| 54 |
+
# Convert attention tensors to lists with rounded decimals
|
| 55 |
+
attn_series = t.round(
|
| 56 |
+
t.tensor([layer_attention.tolist() for layer_attention in attentions], dtype=t.double)
|
| 57 |
+
.squeeze(), decimals=decimals
|
| 58 |
+
).detach().cpu().tolist()
|
| 59 |
+
|
| 60 |
+
return {
|
| 61 |
+
"tokens": tokenizer.convert_ids_to_tokens(inputs["input_ids"][0]),
|
| 62 |
+
"attention": attn_series
|
| 63 |
+
}
|
| 64 |
+
except Exception as e:
|
| 65 |
+
logger.error(f"Error processing text: {e}")
|
| 66 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 67 |
+
|
| 68 |
+
# Initialize the text generation pipeline (unchanged)
|
| 69 |
+
pipe = pipeline("text2text-generation", model="google/flan-t5-small")
|
| 70 |
+
|
| 71 |
+
@app.get("/generate")
|
| 72 |
+
def generate(text: str):
|
| 73 |
+
"""
|
| 74 |
+
Using the text2text-generation pipeline from `transformers`, generate text
|
| 75 |
+
from the given input text. The model used is `google/flan-t5-small`.
|
| 76 |
+
"""
|
| 77 |
+
# Use the pipeline to generate text from the given input text
|
| 78 |
+
output = pipe(text)
|
| 79 |
+
# Return the generated text in a JSON response
|
| 80 |
+
return {"output": output[0]["generated_text"]}
|
| 81 |
+
|
| 82 |
+
@app.get("/")
|
| 83 |
+
async def read_index():
|
| 84 |
+
return FileResponse("frontend/index.html")
|
backend/requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
uvicorn[standard]
|
| 2 |
+
fastapi==0.115.7
|
| 3 |
+
torch==2.5.1
|
| 4 |
+
transformers==4.48.1
|
| 5 |
+
python-multipart>=0.0.5
|
frontend/ model_view.js
ADDED
|
File without changes
|
frontend/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
frontend/index.html
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
| 6 |
+
<title>DeepGaze</title>
|
| 7 |
+
<link rel="stylesheet" href="static/styles.css" />
|
| 8 |
+
<!-- Load D3 -->
|
| 9 |
+
<script src="https://d3js.org/d3.v6.min.js"></script>
|
| 10 |
+
<style>
|
| 11 |
+
/* Process button styling */
|
| 12 |
+
.text-form button {
|
| 13 |
+
background-color: #800000; /* Bordo red */
|
| 14 |
+
color: #fff;
|
| 15 |
+
border: none;
|
| 16 |
+
padding: 8px 12px;
|
| 17 |
+
font-size: 14px;
|
| 18 |
+
border-radius: 4px;
|
| 19 |
+
cursor: pointer;
|
| 20 |
+
}
|
| 21 |
+
/* Info container styling for head and layer display */
|
| 22 |
+
.info-container {
|
| 23 |
+
display: flex;
|
| 24 |
+
align-items: center;
|
| 25 |
+
margin: 5px 0;
|
| 26 |
+
font-size: 0.9rem;
|
| 27 |
+
}
|
| 28 |
+
.info-container .label {
|
| 29 |
+
margin-right: 5px;
|
| 30 |
+
}
|
| 31 |
+
.info-container .number-box {
|
| 32 |
+
border: 1px solid #800000;
|
| 33 |
+
border-radius: 4px;
|
| 34 |
+
padding: 2px 6px;
|
| 35 |
+
font-weight: bold;
|
| 36 |
+
color: #800000;
|
| 37 |
+
min-width: 20px;
|
| 38 |
+
text-align: center;
|
| 39 |
+
}
|
| 40 |
+
</style>
|
| 41 |
+
</head>
|
| 42 |
+
<body>
|
| 43 |
+
<!-- Header -->
|
| 44 |
+
<header>
|
| 45 |
+
<h1>DeepGaze</h1>
|
| 46 |
+
</header>
|
| 47 |
+
<!-- Main container -->
|
| 48 |
+
<div class="container">
|
| 49 |
+
<main>
|
| 50 |
+
<!-- About Attention Section -->
|
| 51 |
+
<section class="about-section">
|
| 52 |
+
<h2>About Attention</h2>
|
| 53 |
+
<p>
|
| 54 |
+
Transformer networks (the architecture behind Chat-GPT) are built from multiple layers, and each layer is divided into several attention heads.
|
| 55 |
+
Each head computes its own attention matrix by combining "queries" and "keys"—the fundamental elements that help
|
| 56 |
+
the network decide how much focus to give to different parts of the input.
|
| 57 |
+
</p>
|
| 58 |
+
<p>
|
| 59 |
+
You can think of each query as a question that a token asks, such as "Are there adjectives in front of me?"
|
| 60 |
+
Meanwhile, each key serves as a potential answer, carrying the token's characteristics. When the model compares
|
| 61 |
+
queries with keys, it determines the strength of their match and, therefore, how much influence one token should
|
| 62 |
+
have on another.
|
| 63 |
+
</p>
|
| 64 |
+
<p>
|
| 65 |
+
For example, consider the phrase "fluffy blue monster." One token might generate a query like, "Is the word in front
|
| 66 |
+
of me an adjective?" In this case, the tokens "fluffy" and "blue"—which are adjectives—provide keys that answer this
|
| 67 |
+
question strongly, while "monster," being a noun, offers a weaker response. This interplay of questions (queries)
|
| 68 |
+
and answers (keys) is what creates the attention matrix for each head.
|
| 69 |
+
</p>
|
| 70 |
+
<p>
|
| 71 |
+
Each attention head focuses on different relationships and patterns within the text, allowing the network to capture
|
| 72 |
+
a rich and nuanced understanding of the language. Despite the critical role that these attention mechanisms play,
|
| 73 |
+
it's interesting to note that only about one third of all the weights in a large language model are actually in the
|
| 74 |
+
attention blocks. So while the famous slogan "attention is all you need" highlights the importance of these connections,
|
| 75 |
+
in terms of sheer weight, it's only one third of what you really need!
|
| 76 |
+
</p>
|
| 77 |
+
<p id="credits">Made with <3 by Ferdi & Samu. Credits for model view below to <a href="https://jessevig.com/" target="_blank">BertViz</a>.</p>
|
| 78 |
+
</section>
|
| 79 |
+
|
| 80 |
+
<!-- Deep Gaze into Attention Heads Section -->
|
| 81 |
+
<section class="deep-gaze-section">
|
| 82 |
+
<h2>A Deep Gaze into Attention Heads</h2>
|
| 83 |
+
<p>Type in a token sequence (below 50 characters) and hit process. After some loading time, you will be able to see the attention patterns of individual so-called "heads" in the LLM.
|
| 84 |
+
Each head focuses on different aspects of the input text, and by visualizing these patterns, you can gain insights into how the model processes and understands language.
|
| 85 |
+
</p>
|
| 86 |
+
|
| 87 |
+
<p>Here is an example view of a head, with tokens on each side. If you see a connection between two tokens, it means that the head is paying attention to the relationship between those tokens. This way you can see attention heads which "pay attention" to the previous token, the first token, or other patterns.
|
| 88 |
+
Click on an attention head to select the respective head in a layer. Afterwards you can hover over tokens to see the attention weights of the selected head for that token. </p>
|
| 89 |
+
<div id="thumbnailContainer"></div>
|
| 90 |
+
<!-- Text Input & Process Button -->
|
| 91 |
+
<form id="textForm" class="text-form">
|
| 92 |
+
<textarea id="inputText" rows="2" cols="50" maxlength="50" placeholder="Enter your text here..." autofocus></textarea>
|
| 93 |
+
<button type="submit">Process</button>
|
| 94 |
+
</form>
|
| 95 |
+
</section>
|
| 96 |
+
|
| 97 |
+
<!-- Model View Section -->
|
| 98 |
+
<section class="model_view">
|
| 99 |
+
<p>Click on a head that looks interesting to gaze deeper into it in the next section:</p>
|
| 100 |
+
|
| 101 |
+
<div id="model_view_container">
|
| 102 |
+
<!-- Thumbnails of attention heads will be rendered here -->
|
| 103 |
+
</div>
|
| 104 |
+
<!-- Display for selected head and layer -->
|
| 105 |
+
<div id="display_info">
|
| 106 |
+
<div id="display_head" class="info-container">
|
| 107 |
+
<span class="label">Head:</span>
|
| 108 |
+
<span class="number-box">-</span>
|
| 109 |
+
</div>
|
| 110 |
+
<div id="display_layer" class="info-container">
|
| 111 |
+
<span class="label">Layer:</span>
|
| 112 |
+
<span class="number-box">-</span>
|
| 113 |
+
</div>
|
| 114 |
+
</div>
|
| 115 |
+
</section>
|
| 116 |
+
|
| 117 |
+
<!-- Hover Visualization Section -->
|
| 118 |
+
<section class="hover-visualization">
|
| 119 |
+
<h2>Hover Visualization</h2>
|
| 120 |
+
<p>
|
| 121 |
+
By hovering over each token, you can see which other token is important for that token. The larger the token, the more important it is for the token you are hovering over. The token with the maximal attention is colored in red.
|
| 122 |
+
</p>
|
| 123 |
+
<div id="tokenContainer"></div>
|
| 124 |
+
</section>
|
| 125 |
+
</main>
|
| 126 |
+
</div>
|
| 127 |
+
|
| 128 |
+
<!-- External JavaScript (all event listeners and functions are defined here) -->
|
| 129 |
+
<script src="static/script.js?v=1111"></script>
|
| 130 |
+
</body>
|
| 131 |
+
</html>
|
frontend/script.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Form submit handler 2.1
|
| 2 |
+
document.getElementById('textForm').addEventListener('submit', async (e) => {
|
| 3 |
+
e.preventDefault();
|
| 4 |
+
const inputText = document.getElementById('inputText').value;
|
| 5 |
+
|
| 6 |
+
try {
|
| 7 |
+
const response = await fetch('/process', {
|
| 8 |
+
method: 'POST',
|
| 9 |
+
headers: { 'Content-Type': 'application/json' },
|
| 10 |
+
body: JSON.stringify({ text: inputText })
|
| 11 |
+
});
|
| 12 |
+
|
| 13 |
+
if (!response.ok) {
|
| 14 |
+
throw new Error('Network response was not ok');
|
| 15 |
+
}
|
| 16 |
+
const data = await response.json();
|
| 17 |
+
|
| 18 |
+
// Use data.tokens and data.attention from your POST response
|
| 19 |
+
// displayOutput(data);
|
| 20 |
+
displayHoverTokens(data, 0, 0);
|
| 21 |
+
// Changed call here to pass the entire data object
|
| 22 |
+
renderModelView(data);
|
| 23 |
+
} catch (error) {
|
| 24 |
+
console.error('Error:', error);
|
| 25 |
+
document.getElementById('output').innerText = 'Error processing text.';
|
| 26 |
+
}
|
| 27 |
+
});
|
| 28 |
+
|
| 29 |
+
function renderModelView(data) {
|
| 30 |
+
// Extract tokens and attention from the data object
|
| 31 |
+
const tokens = data.tokens;
|
| 32 |
+
const attention = data.attention;
|
| 33 |
+
|
| 34 |
+
const container = document.getElementById("model_view_container");
|
| 35 |
+
if (!container) return;
|
| 36 |
+
container.innerHTML = "";
|
| 37 |
+
|
| 38 |
+
const gridContainer = document.createElement("div");
|
| 39 |
+
gridContainer.style.display = "grid";
|
| 40 |
+
gridContainer.style.gridTemplateColumns = "repeat(12, 80px)";
|
| 41 |
+
gridContainer.style.gridGap = "10px";
|
| 42 |
+
gridContainer.style.padding = "20px";
|
| 43 |
+
|
| 44 |
+
// Loop over all 12 layers and 12 heads, passing the complete data object
|
| 45 |
+
for (let layerIdx = 0; layerIdx < 12; layerIdx++) {
|
| 46 |
+
for (let headIdx = 0; headIdx < 12; headIdx++) {
|
| 47 |
+
const thumbnail = createAttentionThumbnail(data, layerIdx, headIdx);
|
| 48 |
+
gridContainer.appendChild(thumbnail);
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
container.appendChild(gridContainer);
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
function createAttentionThumbnail(data, layerIdx, headIdx) {
|
| 56 |
+
// Extract tokens and attention from the data object
|
| 57 |
+
const tokens = data.tokens;
|
| 58 |
+
const attention = data.attention;
|
| 59 |
+
|
| 60 |
+
const width = 80;
|
| 61 |
+
const tokenHeight = 15;
|
| 62 |
+
const padding = 10;
|
| 63 |
+
// Compute the thumbnail height dynamically based on the number of tokens.
|
| 64 |
+
const height = padding * 2 + tokens.length * tokenHeight;
|
| 65 |
+
const maxLineWidth = 4;
|
| 66 |
+
const maxOpacity = 0.8;
|
| 67 |
+
|
| 68 |
+
// Compute the right-side x-coordinate numerically.
|
| 69 |
+
const xRight = width - padding;
|
| 70 |
+
|
| 71 |
+
// Create a thumbnail container using D3.
|
| 72 |
+
const thumbnail = d3.select(document.createElement("div"))
|
| 73 |
+
.style("position", "relative")
|
| 74 |
+
.style("height", height + "px")
|
| 75 |
+
.style("width", width + "px")
|
| 76 |
+
.style("border", "1px solid #ddd")
|
| 77 |
+
.style("border-radius", "4px")
|
| 78 |
+
.style("padding", "5px")
|
| 79 |
+
.style("background", "#fff");
|
| 80 |
+
|
| 81 |
+
// Append an SVG container with fixed dimensions.
|
| 82 |
+
const svg = thumbnail.append("svg")
|
| 83 |
+
.attr("width", width)
|
| 84 |
+
.attr("height", height);
|
| 85 |
+
|
| 86 |
+
// Add header text (e.g., "L4 H4") to show the layer and head number.
|
| 87 |
+
svg.append("text")
|
| 88 |
+
.attr("x", width / 2)
|
| 89 |
+
.attr("y", 15)
|
| 90 |
+
.attr("text-anchor", "middle")
|
| 91 |
+
.attr("font-size", "10")
|
| 92 |
+
.text(`L${layerIdx + 1} H${headIdx + 1}`);
|
| 93 |
+
|
| 94 |
+
// Draw attention lines with per-row normalization.
|
| 95 |
+
attention[layerIdx][headIdx].forEach((sourceWeights, sourceIdx) => {
|
| 96 |
+
const rowMax = Math.max(...sourceWeights) || 1;
|
| 97 |
+
sourceWeights.forEach((weight, targetIdx) => {
|
| 98 |
+
if (weight > 0.01 && sourceIdx !== targetIdx) {
|
| 99 |
+
const normalizedWeight = weight / rowMax;
|
| 100 |
+
svg.append("line")
|
| 101 |
+
.attr("x1", padding)
|
| 102 |
+
.attr("y1", padding + sourceIdx * tokenHeight - 5)
|
| 103 |
+
.attr("x2", xRight)
|
| 104 |
+
.attr("y2", padding + targetIdx * tokenHeight - 5)
|
| 105 |
+
.attr("stroke", "#800000") // Bordo red
|
| 106 |
+
.attr("stroke-width", Math.max(0.5, normalizedWeight * maxLineWidth))
|
| 107 |
+
.attr("opacity", Math.min(maxOpacity, normalizedWeight * 2))
|
| 108 |
+
.attr("stroke-linecap", "round");
|
| 109 |
+
}
|
| 110 |
+
});
|
| 111 |
+
});
|
| 112 |
+
|
| 113 |
+
// Click handler remains unchanged, using the passed-in data object.
|
| 114 |
+
thumbnail.on("click", function() {
|
| 115 |
+
d3.select("#display_head .number-box").text(headIdx + 1);
|
| 116 |
+
d3.select("#display_layer .number-box").text(layerIdx + 1);
|
| 117 |
+
displayHoverTokens(data, layerIdx, headIdx);
|
| 118 |
+
});
|
| 119 |
+
|
| 120 |
+
return thumbnail.node();
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
// Function to display the tokens and attention values
|
| 124 |
+
// function displayOutput(data) {
|
| 125 |
+
// const outputDiv = document.getElementById('output');
|
| 126 |
+
// outputDiv.innerHTML = `
|
| 127 |
+
// <h2>Tokens</h2>
|
| 128 |
+
// <pre>${JSON.stringify(data.tokens, null, 2)}</pre>
|
| 129 |
+
// <h2>Attention</h2>
|
| 130 |
+
// <pre>${JSON.stringify(data.attention, null, 2)}</pre>
|
| 131 |
+
// `;
|
| 132 |
+
// }
|
| 133 |
+
|
| 134 |
+
function renderTokens(tokens, attentionData, layer_idx, head_idx) {
|
| 135 |
+
const container = document.getElementById('tokenContainer');
|
| 136 |
+
container.innerHTML = "";
|
| 137 |
+
|
| 138 |
+
tokens.forEach((token, index) => {
|
| 139 |
+
const span = document.createElement('span');
|
| 140 |
+
span.textContent = token.replace("Ġ", "") + " ";
|
| 141 |
+
span.style.fontSize = "32px";
|
| 142 |
+
span.addEventListener('mouseenter', () => {
|
| 143 |
+
highlightAttention(index, attentionData, layer_idx, head_idx);
|
| 144 |
+
});
|
| 145 |
+
span.addEventListener('mouseleave', () => {
|
| 146 |
+
resetTokenSizes();
|
| 147 |
+
});
|
| 148 |
+
container.appendChild(span);
|
| 149 |
+
});
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
function displayHoverTokens(data, layer_idx, head_idx) {
|
| 153 |
+
let tokens, attentionMatrix;
|
| 154 |
+
if (!data.tokens || !data.attention) {
|
| 155 |
+
tokens = ['This', 'is', 'a', 'test', '.'];
|
| 156 |
+
// Create a dummy attention matrix if missing
|
| 157 |
+
attentionMatrix = Array(12)
|
| 158 |
+
.fill(null)
|
| 159 |
+
.map(() => Array(12).fill(null).map(() => Array(tokens.length).fill(0)));
|
| 160 |
+
} else {
|
| 161 |
+
tokens = data.tokens;
|
| 162 |
+
attentionMatrix = data.attention;
|
| 163 |
+
}
|
| 164 |
+
renderTokens(tokens, attentionMatrix, layer_idx, head_idx);
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
function resetTokenSizes() {
|
| 168 |
+
const container = document.getElementById("tokenContainer");
|
| 169 |
+
Array.from(container.children).forEach((span) => {
|
| 170 |
+
span.style.fontSize = "32px";
|
| 171 |
+
span.style.color = "#555";
|
| 172 |
+
});
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
function highlightAttention(index, attentionData, layer_idx, head_idx) {
|
| 176 |
+
const container = document.getElementById('tokenContainer');
|
| 177 |
+
const row = attentionData[layer_idx][head_idx][index];
|
| 178 |
+
|
| 179 |
+
if (!row) {
|
| 180 |
+
console.warn(`No attention data for token index ${index}`);
|
| 181 |
+
return;
|
| 182 |
+
}
|
| 183 |
+
const weights = row;
|
| 184 |
+
if (!weights.length) {
|
| 185 |
+
return;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
// Find the maximum weight
|
| 189 |
+
const maxWeight = Math.max(...weights) || 1;
|
| 190 |
+
const baseFontSize = 32;
|
| 191 |
+
const maxIncrease = 20;
|
| 192 |
+
|
| 193 |
+
const maxIndex = weights.indexOf(maxWeight);
|
| 194 |
+
|
| 195 |
+
Array.from(container.children).forEach((span, idx) => {
|
| 196 |
+
const weight = weights[idx];
|
| 197 |
+
|
| 198 |
+
if (typeof weight === 'number') {
|
| 199 |
+
const newFontSize = baseFontSize + (weight / maxWeight) * maxIncrease;
|
| 200 |
+
span.style.fontSize = newFontSize + "px";
|
| 201 |
+
|
| 202 |
+
if (idx === maxIndex) {
|
| 203 |
+
span.style.color = "#800000"; // Bordo red
|
| 204 |
+
} else {
|
| 205 |
+
span.style.color = "#555"; // Reset color
|
| 206 |
+
}
|
| 207 |
+
} else {
|
| 208 |
+
// For tokens without a corresponding weight, reset styles.
|
| 209 |
+
span.style.fontSize = baseFontSize + "px";
|
| 210 |
+
span.style.color = "#555";
|
| 211 |
+
}
|
| 212 |
+
});
|
| 213 |
+
}
|
frontend/styles.css
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap');
|
| 2 |
+
@import url('https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400..700;1,400..700&display=swap');
|
| 3 |
+
|
| 4 |
+
*,
|
| 5 |
+
*::before,
|
| 6 |
+
*::after {
|
| 7 |
+
box-sizing: border-box;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
/* Basic Resets & Body */
|
| 11 |
+
html, body {
|
| 12 |
+
margin: 0;
|
| 13 |
+
padding: 0;
|
| 14 |
+
font-family: 'Lora', 'Roboto', sans-serif;
|
| 15 |
+
background-color: #fefdf6; /* Off-white background */
|
| 16 |
+
color: #333;
|
| 17 |
+
}
|
| 18 |
+
input:focus,
|
| 19 |
+
select:focus,
|
| 20 |
+
textarea:focus,
|
| 21 |
+
button:focus {
|
| 22 |
+
outline: none;
|
| 23 |
+
}
|
| 24 |
+
/* Header spanning full width */
|
| 25 |
+
header {
|
| 26 |
+
width: 100%;
|
| 27 |
+
background-color: #fff;
|
| 28 |
+
padding: 1rem 2rem;
|
| 29 |
+
border-bottom: 1px solid #ddd;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
header h1 {
|
| 33 |
+
margin: 0;
|
| 34 |
+
font-size: 1.8rem;
|
| 35 |
+
font-weight: 100;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
/*
|
| 39 |
+
Container that splits the page:
|
| 40 |
+
left (main) ~75%, right (aside) ~25%
|
| 41 |
+
*/
|
| 42 |
+
.container {
|
| 43 |
+
display: flex;
|
| 44 |
+
flex-direction: row;
|
| 45 |
+
min-height: calc(100vh - 60px); /* Keep some height below header */
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
/* Main content on the left */
|
| 49 |
+
.container main {
|
| 50 |
+
width: 75%;
|
| 51 |
+
padding: 2rem;
|
| 52 |
+
box-sizing: border-box;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
/* The aside on the right can be used or left empty */
|
| 56 |
+
.container aside {
|
| 57 |
+
width: 25%;
|
| 58 |
+
padding: 2rem;
|
| 59 |
+
box-sizing: border-box;
|
| 60 |
+
background-color: #fffff8;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
/* ABOUT / DEEP GAZE SECTIONS */
|
| 64 |
+
.about-section {
|
| 65 |
+
margin-bottom: 2rem;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.about-section h2 {
|
| 69 |
+
font-size: 1.4rem;
|
| 70 |
+
margin-bottom: 1rem;
|
| 71 |
+
font-weight: 100;
|
| 72 |
+
font-style: italic;
|
| 73 |
+
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
.about-section p {
|
| 77 |
+
line-height: 1.6;
|
| 78 |
+
margin-bottom: 1rem;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
.deep-gaze-section h2 {
|
| 82 |
+
font-size: 1.4rem;
|
| 83 |
+
margin-bottom: 0.5rem;
|
| 84 |
+
font-weight: 100;
|
| 85 |
+
font-style: italic;
|
| 86 |
+
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
.deep-gaze-section p {
|
| 90 |
+
margin-bottom: 1rem;
|
| 91 |
+
line-height: 1.6;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
/* Selection & Text Form Styles */
|
| 95 |
+
.selection-form,
|
| 96 |
+
.text-form {
|
| 97 |
+
margin-bottom: 1.5rem;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
.selection-form label {
|
| 101 |
+
margin-right: 0.5rem;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
.selection-form select {
|
| 105 |
+
margin-right: 1rem;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.text-form {
|
| 109 |
+
display: flex;
|
| 110 |
+
flex-direction: column;
|
| 111 |
+
align-items: flex-start;
|
| 112 |
+
border: none;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
textarea {
|
| 116 |
+
width: 100%;
|
| 117 |
+
max-width: 100%;
|
| 118 |
+
font-size: 2rem;
|
| 119 |
+
font-weight: 700;
|
| 120 |
+
margin-bottom: 2rem;
|
| 121 |
+
border: none;
|
| 122 |
+
resize: none;
|
| 123 |
+
font-family: 'Roboto', sans-serif;
|
| 124 |
+
background-color: #fffff8;
|
| 125 |
+
color: #3a3939;
|
| 126 |
+
border-radius: 8px;
|
| 127 |
+
padding: 20px;
|
| 128 |
+
filter: drop-shadow(0 0 0.75rem #ddd);
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
textarea::placeholder {
|
| 133 |
+
color: #888;
|
| 134 |
+
font-family: 'Roboto', sans-serif;
|
| 135 |
+
font-size:2rem;
|
| 136 |
+
font-weight: 700;
|
| 137 |
+
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
button {
|
| 141 |
+
background-color: #4b80f9;
|
| 142 |
+
color: white;
|
| 143 |
+
border: none;
|
| 144 |
+
border-radius: 4px;
|
| 145 |
+
padding: 0.6rem 1rem;
|
| 146 |
+
cursor: pointer;
|
| 147 |
+
font-size: 1rem;
|
| 148 |
+
}
|
| 149 |
+
button:hover {
|
| 150 |
+
background-color: #3f6ddb;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
/* Output Area */
|
| 154 |
+
#output {
|
| 155 |
+
background-color: #fff;
|
| 156 |
+
border: 1px solid #ddd;
|
| 157 |
+
padding: 1rem;
|
| 158 |
+
margin-bottom: 1rem;
|
| 159 |
+
max-height: 300px;
|
| 160 |
+
overflow-y: auto;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
#output h2 {
|
| 164 |
+
margin-top: 0;
|
| 165 |
+
font-size: 1.2rem;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
/* Token Container */
|
| 169 |
+
#tokenContainer {
|
| 170 |
+
margin-top: 1rem;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
/* Token highlighting styles */
|
| 174 |
+
#tokenContainer span {
|
| 175 |
+
cursor: default;
|
| 176 |
+
transition: font-size 0.9s ease;
|
| 177 |
+
color: #555;
|
| 178 |
+
display: inline-block;
|
| 179 |
+
transition: font-size 0.9s ease;
|
| 180 |
+
margin-right: 4px;
|
| 181 |
+
padding: 2px 4px;
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
.text-form button {
|
| 185 |
+
background-color: #800000; /* Bordo red */
|
| 186 |
+
color: #fff; /* Optional: set text color to white for contrast */
|
| 187 |
+
border: none;
|
| 188 |
+
padding: 10px 20px;
|
| 189 |
+
font-size: 16px;
|
| 190 |
+
cursor: pointer;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
#credits {
|
| 194 |
+
font-size: 0.8rem;
|
| 195 |
+
color: #888;
|
| 196 |
+
margin-top: 1rem;
|
| 197 |
+
font-style: italic;
|
| 198 |
+
}
|