Spaces:
Running
Running
Create relationship_ui.py
Browse files- relationship_ui.py +97 -0
relationship_ui.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Relationship controls; analysis snapshots are scoped to model and tensor."""
|
| 2 |
+
import numpy as np
|
| 3 |
+
import streamlit as st
|
| 4 |
+
|
| 5 |
+
from relationships import analyse_pairs
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def render_relationship_controls(vectors, label, scope, save_calculation):
|
| 9 |
+
options = list(vectors)
|
| 10 |
+
active_key = f'active_relationship:{scope}'
|
| 11 |
+
example_key = f'relationship_examples:{scope}'
|
| 12 |
+
|
| 13 |
+
def capture(key):
|
| 14 |
+
return {'key': key, 'label': label(key), 'values': np.array(vectors[key], dtype=float, copy=True)}
|
| 15 |
+
|
| 16 |
+
def choose(text, suffix, index=0, optional=False):
|
| 17 |
+
choices = [None] + options if optional else options
|
| 18 |
+
return st.selectbox(text, choices, index=0 if optional else min(index, len(choices) - 1),
|
| 19 |
+
format_func=lambda key: 'None' if key is None else label(key),
|
| 20 |
+
key=f'rel_{suffix}:{scope}')
|
| 21 |
+
|
| 22 |
+
def remove_example(identifier):
|
| 23 |
+
st.session_state[example_key] = [p for p in st.session_state[example_key] if p['id'] != identifier]
|
| 24 |
+
|
| 25 |
+
st.caption('A → B is to C → D. Show relationship applies these inputs to the chart.')
|
| 26 |
+
if len(options) < 2:
|
| 27 |
+
st.info('Add at least two tokens or calculated vectors to the chart to choose a relationship.')
|
| 28 |
+
return st.session_state.get(active_key)
|
| 29 |
+
mode = st.selectbox('Relationship task', ['Compare relationships', 'Complete analogy', 'Average example pairs'],
|
| 30 |
+
key=f'rel_task:{scope}')
|
| 31 |
+
if mode == 'Average example pairs':
|
| 32 |
+
example_a = choose('Example A', 'example_a')
|
| 33 |
+
example_b = choose('Example B', 'example_b', 1)
|
| 34 |
+
pairs = st.session_state.setdefault(example_key, [])
|
| 35 |
+
if st.button('Add example pair', key='add_relationship_example', disabled=len(pairs) >= 10):
|
| 36 |
+
serial = st.session_state.get('relationship_example_serial', 0) + 1
|
| 37 |
+
st.session_state.relationship_example_serial = serial
|
| 38 |
+
pairs.append({'id': serial, 'a': capture(example_a), 'b': capture(example_b)})
|
| 39 |
+
for number, pair in enumerate(pairs, 1):
|
| 40 |
+
text, action = st.columns([3, 1.2], vertical_alignment='center')
|
| 41 |
+
text.text(f'{number}. {pair["a"]["label"]} → {pair["b"]["label"]}')
|
| 42 |
+
action.button('Remove', key=f'remove_relationship_example:{pair["id"]}',
|
| 43 |
+
on_click=remove_example, args=(pair['id'],))
|
| 44 |
+
st.caption(f'{len(pairs)} / 10 example pairs · offsets are averaged equally')
|
| 45 |
+
else:
|
| 46 |
+
source_a = choose('A', 'a')
|
| 47 |
+
source_b = choose('B', 'b', 1)
|
| 48 |
+
pairs = [{'a': capture(source_a), 'b': capture(source_b)}]
|
| 49 |
+
target = choose('C (apply relationship to)', 'c', 2)
|
| 50 |
+
expected = choose('D (compare against)' if mode == 'Compare relationships' else 'Expected D (optional)',
|
| 51 |
+
'd' if mode == 'Compare relationships' else 'expected', 3,
|
| 52 |
+
optional=mode != 'Compare relationships')
|
| 53 |
+
if st.button('Show relationship', key='show_relationship', type='primary', disabled=not pairs, use_container_width=True):
|
| 54 |
+
try:
|
| 55 |
+
target_data = capture(target)
|
| 56 |
+
expected_data = capture(expected) if expected is not None else None
|
| 57 |
+
metrics = analyse_pairs([(p['a']['values'], p['b']['values']) for p in pairs],
|
| 58 |
+
target_data['values'], expected_data['values'] if expected_data else None)
|
| 59 |
+
serial = st.session_state.get('relationship_serial', 0) + 1
|
| 60 |
+
st.session_state.relationship_serial = serial
|
| 61 |
+
excluded = {item['key'] for p in pairs for item in [p['a'], p['b']] if isinstance(item['key'], int)}
|
| 62 |
+
if isinstance(target, int):
|
| 63 |
+
excluded.add(target)
|
| 64 |
+
st.session_state[active_key] = {'id': serial, 'mode': mode, 'pairs': list(pairs),
|
| 65 |
+
'target': target_data, 'expected': expected_data,
|
| 66 |
+
'metrics': metrics, 'exclude_ids': sorted(excluded)}
|
| 67 |
+
st.session_state.pending_chart_view = 'Relationship 3D'
|
| 68 |
+
st.rerun()
|
| 69 |
+
except ValueError as e:
|
| 70 |
+
st.error(str(e))
|
| 71 |
+
analysis = st.session_state.get(active_key)
|
| 72 |
+
if analysis is None:
|
| 73 |
+
return None
|
| 74 |
+
metrics = analysis['metrics']
|
| 75 |
+
st.caption('Full-dimensional scores for the last shown relationship. No projection is used for these calculations.')
|
| 76 |
+
if 'direction_cosine' in metrics:
|
| 77 |
+
left, right = st.columns(2)
|
| 78 |
+
left.metric('Offset cosine', f'{metrics["direction_cosine"]:.4f}' if metrics['direction_cosine'] is not None else 'Undefined')
|
| 79 |
+
right.metric('Angle', f'{metrics["angle_degrees"]:.1f}°' if metrics['angle_degrees'] is not None else 'Undefined')
|
| 80 |
+
left.metric('Length ratio D−C / offset', f'{metrics["length_ratio"]:.4f}' if metrics['length_ratio'] is not None else 'Undefined')
|
| 81 |
+
right.metric('Prediction error (L2)', f'{metrics["prediction_error"]:.5f}')
|
| 82 |
+
else:
|
| 83 |
+
st.metric('Mean offset length', f'{metrics["offset_length"]:.5f}')
|
| 84 |
+
if len(analysis['pairs']) > 1:
|
| 85 |
+
agreement = metrics['example_agreement']
|
| 86 |
+
st.caption(f'Example offsets vs average: mean cosine {agreement:.4f}' if agreement is not None else 'Example direction agreement is undefined.')
|
| 87 |
+
if metrics['offset_length'] == 0 or metrics.get('observed_length') == 0:
|
| 88 |
+
st.info('A zero-length relationship has no direction. Direction cosine and angle are undefined for it.')
|
| 89 |
+
if st.button('Find analogy answers', key='find_analogy_answers', use_container_width=True):
|
| 90 |
+
st.session_state[f'neighbour_mode:{scope}'] = 'Analogy answers'
|
| 91 |
+
st.session_state[f'run_analogy:{scope}'] = True
|
| 92 |
+
if st.button('Add prediction as calc', key='save_relationship_prediction', disabled=len(vectors) >= 20, use_container_width=True):
|
| 93 |
+
save_calculation(metrics['prediction'], f'analogy prediction #{analysis["id"]}')
|
| 94 |
+
if st.button('Add mean offset as calc', key='save_relationship_offset', disabled=len(vectors) >= 20, use_container_width=True):
|
| 95 |
+
save_calculation(metrics['offset'], f'mean relationship offset #{analysis["id"]}')
|
| 96 |
+
st.caption('Prediction = C + mean(B − A). Analogy searches exclude the input vocabulary tokens. Scores use stored, unnormalised input vectors.')
|
| 97 |
+
return analysis
|