13
This commit is contained in:
parent
7ebbc68800
commit
79abfab44c
File diff suppressed because one or more lines are too long
407
wyk/13_Atencja.ipynb
Normal file
407
wyk/13_Atencja.ipynb
Normal file
@ -0,0 +1,407 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"![Logo 1](https://git.wmi.amu.edu.pl/AITech/Szablon/raw/branch/master/Logotyp_AITech1.jpg)\n",
|
||||
"<div class=\"alert alert-block alert-info\">\n",
|
||||
"<h1> Modelowanie języka</h1>\n",
|
||||
"<h2> 13. <i>Atencja</i> [wykład]</h2> \n",
|
||||
"<h3> Filip Graliński (2022)</h3>\n",
|
||||
"</div>\n",
|
||||
"\n",
|
||||
"![Logo 2](https://git.wmi.amu.edu.pl/AITech/Szablon/raw/branch/master/Logotyp_AITech2.jpg)\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Atencja\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Sieci LSTM w roku 2017/2018 zostały wyparte przez nową, pod pewnymi\n",
|
||||
"względami prostszą, architekturę Transformer. Sieci Transformer oparte\n",
|
||||
"są zasadniczo na prostej idei **atencji** (*attention*), pierwszy\n",
|
||||
"artykuł wprowadzający sieci Transformer nosił nawet tytuł\n",
|
||||
"[Attention Is All You Need]([https://arxiv.org/abs/1706.03762](https://arxiv.org/abs/1706.03762)).\n",
|
||||
"\n",
|
||||
"Intuicyjnie, atencja jest rodzajem uwagi, którą sieć może selektywnie\n",
|
||||
"kierować na wybrane miejsca (w modelowaniu języka: wybrane wyrazy).\n",
|
||||
"\n",
|
||||
"Idea atencji jest jednak wcześniejsza, powstała jako ulepszenie sieci\n",
|
||||
"rekurencyjnych. My omówimy ją jednak na jeszcze prostszym przykładzie\n",
|
||||
"użycia w modelowaniu języka za pomocą hybrydy modelu\n",
|
||||
"bigramowego i modelu worka słów.\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Prosty przykład zastosowania atencji\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Wróćmy do naszego przykładu z Wykładu 8, w którym łączyliśmy $n$-gramowy\n",
|
||||
"model języka z workiem słów. Przyjmijmy bigramowy model języka ($n=2$), wówczas:\n",
|
||||
"\n",
|
||||
"$$y = \\operatorname{softmax}(C[E(w_{i-1}),A(w_1,\\dots,w_{i-2})]),$$\n",
|
||||
"\n",
|
||||
"gdzie $A$ była prostą agregacją (np. sumą albo średnią) embeddingów\n",
|
||||
"$E(w_1),\\dots,E(w_{i-2})$. Aby wyjść z nieuporządkowanego\n",
|
||||
"modelu worka słów, próbowaliśmy w prosty sposób uwzględnić pozycję\n",
|
||||
"wyrazów czy ich istotność (za pomocą odwrotnej częstości\n",
|
||||
"dokumentowej). Oba te sposoby niestety zupełnie nie uwzględniają **kontekstu**.\n",
|
||||
"\n",
|
||||
"Innymi słowy, chcielibyśmy mieć sumę ważoną zanurzeń:\n",
|
||||
"\n",
|
||||
"$$A(w_1,\\dots,j) = \\omega_1 E(w_1) + \\dots + \\omega_j E(w_j) = \\sum_{k=1}^j \\omega_k E(w_k),$$\n",
|
||||
"\n",
|
||||
"tak by $\\omega_k$ w sposób bardziej zasadniczy zależały od lokalnego kontekstu, a\n",
|
||||
"nie tylko od pozycji $k$ czy słowa $w_k$. W naszym uproszczonym przypadku\n",
|
||||
"jako kontekst możemy rozpatrywać słowo bezpośrednio poprzedzające\n",
|
||||
"odgadywane słowa (kontekstem jest $w_{i-1}$).\n",
|
||||
"\n",
|
||||
"Wygodnie również przyjąć, że $\\sum_{k=1}^j \\omega_k = 1$ i $\\omega_k\n",
|
||||
"\\in (0,1)$, wówczas mamy do czynienia ze średnią ważoną.\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"#### Nieznormalizowane wagi atencji\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Będziemy liczyć nieznormalizowane ****wagi atencji****\n",
|
||||
"$\\hat{\\alpha}_{k,j}$. Określają one, jak bardzo słowo $w_j$ „zwraca\n",
|
||||
"uwagę” na poszczególne, inne słowa. Innymi słowy, wagi atencji opisują, jak\n",
|
||||
"bardzo słowo $w_k$ pasuje do naszego kontekstu, czyli słowa $w_j$.\n",
|
||||
"\n",
|
||||
"**Uwaga**: (nieznormalizowane czy znormalizowane) wagi atencji nie należą do wyuczalnych\n",
|
||||
"wag (parametrów) modelu.\n",
|
||||
"\n",
|
||||
"Najprostszy sposób mierzenia dopasowania to po prostu iloczyn skalarny:\n",
|
||||
"\n",
|
||||
"$$\\hat{\\alpha}_{k,j} = E(w_k)E(w_j),$$\n",
|
||||
"\n",
|
||||
"można też alternatywnie złamać symetrię iloczynu skalarnego i\n",
|
||||
"wyliczać dopasowanie za pomocą prostej sieci feed-forward:\n",
|
||||
"\n",
|
||||
"$$\\hat{\\alpha}_{k,j} =\n",
|
||||
"\\vec{v}\\operatorname{tanh}(W_{\\alpha}[E(w_k),E(w_j)] +\n",
|
||||
"\\vec{b_{\\alpha}}).$$\n",
|
||||
"\n",
|
||||
"W drugim przypadku pojawiają się dodatkowe wyuczalne paramatery: macierz $W_{\\alpha}$, wektory\n",
|
||||
"$\\vec{b_{\\alpha}}$ i $\\vec{v}$.\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"#### Normalizacja wag atencji\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Jak już wspomniano, dobrze żeby wagi atencji sumowały się do 1. W tym celu możemy po prostu zastosować\n",
|
||||
"funkcję softmax:\n",
|
||||
"\n",
|
||||
"$$\\alpha_{k,j} = \\operatorname{softmax}([\\hat{\\alpha}_{1,j},\\dots,\\hat{\\alpha}_{j-1,j}]).$$\n",
|
||||
"\n",
|
||||
"Zauważmy jednak, że otrzymanego z funkcji softmax wektora\n",
|
||||
"$[\\alpha_{1,j},\\dots,\\alpha_{j-1,j}]$ tym razem nie interpretujemy jako rozkład prawdopodobieństwa.\n",
|
||||
"Jest to raczej rozkład uwagi, atencji słowa $w_j$ względem innych słów.\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"#### Użycie wag atencji w prostym neuronowym modelu języka\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Teraz jako wagi $\\omega$ w naszym modelu języka możemy przyjąć:\n",
|
||||
"\n",
|
||||
"$$\\omega_k = \\alpha_{k,i-1}.$$\n",
|
||||
"\n",
|
||||
"Oznacza to, że z naszego worka będziemy „wyjmowali” słowa w sposób\n",
|
||||
"selektywny, w zależności od wyrazu, który bezpośrednio poprzedza\n",
|
||||
"słowo odgadywane.\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"#### Diagram\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"![img](./13_Atencja/simple-attention.drawio.png \"Atencja użyta w prostym neuronowym modelu języka\")\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Atencja jako składnik sieci rekurencyjnej\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Atencję wprowadzono pierwotnie jako uzupełnienie sieci rekurencyjnej.\n",
|
||||
"Potrzeba ta pojawiła się na początku rozwoju **neuronowego tłumaczenia\n",
|
||||
"maszynowego** (*neural machine translation*, *NMT*), czyli tłumaczenia\n",
|
||||
"maszynowego (automatycznego) realizowanego za pomocą sieci neuronowych.\n",
|
||||
"\n",
|
||||
"Neuronowe tłumaczenie maszynowe jest właściwie rozszerzeniem idei\n",
|
||||
"modelowania języka na biteksty (teksty równoległe). Omówmy najpierw\n",
|
||||
"podstawy generowania tekstu.\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"#### Model języka jako generator\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Jak pamiętamy, model języka $M$ wylicza prawdopodobieństwo tekstu $w_1,\\dots,w_N$:\n",
|
||||
"\n",
|
||||
"$$P_M(w_1,\\dots,w_N) = ?.$$\n",
|
||||
"\n",
|
||||
"Zazwyczaj jest to równoważne obliczaniu rozkładu prawdopodobieństwa kolejnego słowa:\n",
|
||||
"\n",
|
||||
"$$P_M(w_j|w_1,\\dots,w_{j-1}) = ?.$$\n",
|
||||
"\n",
|
||||
"Załóżmy, że mamy pewien początek (**prefiks**) tekstu o długości $p$:\n",
|
||||
"$w_1,\\dots,w_p$. Powiedzmy, że naszym celem jest **wygenerowanie**\n",
|
||||
"dokończenia czy kontynuacji tego tekstu (nie określamy z góry\n",
|
||||
"długości tej kontynuacji).\n",
|
||||
"\n",
|
||||
"Najprostszy sposób wygenerowania pierwszego wyrazu dokończenia polega\n",
|
||||
"na wzięciu wyrazu maksymalizującego prawdopodobieństwo według modelu języka:\n",
|
||||
"\n",
|
||||
"$$w_{p+1} = \\operatorname{argmax}_w P_M(w|w_1,\\dots,w_p).$$\n",
|
||||
"\n",
|
||||
"**Pytanie**: Dlaczego $\\operatorname{argmax}$, a nie $\\operatorname{max}$?\n",
|
||||
"\n",
|
||||
"Słowo $w_{p+1}$ możemy dołączyć do prefiksu i powtórzyć procedurę:\n",
|
||||
"\n",
|
||||
"$$w_{p+2} = \\operatorname{argmax}_w P_M(w|w_1,\\dots,w_p,w_{p+1}),$$\n",
|
||||
"\n",
|
||||
"i tak dalej.\n",
|
||||
"\n",
|
||||
"**Pytanie**: Kiedy zakończymy procedurę generowania?\n",
|
||||
"\n",
|
||||
"Omawiana procedura jest najprostszym sposobem, czasami nie daje\n",
|
||||
"najlepszego wyniku, na przykład może pojawić się efekt „jąkania”\n",
|
||||
"(model generuje w kółko ten sam wyraz), dlatego opracowano bardziej\n",
|
||||
"wymyślne sposoby generowania w oparciu o modele języka. Omówimy je później.\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"#### Zastosowania generatora opartego na modelu języka\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Mogłoby się wydawać, że generator tekstu ma raczej ograniczone\n",
|
||||
"zastosowanie (generowanie fake newsów?). Okazuje się jednak, że\n",
|
||||
"zaskakująco wiele zadań przetwarzania języka naturalnego można\n",
|
||||
"przedstawić jako zadanie generowania tekstu. Przykładem jest tłumaczenie maszynowe.\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"#### Tłumaczenie maszynowe jako zadanie generowania tekstu\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"W tłumaczeniu maszynowym (tłumaczeniu automatycznym, ang. *machine\n",
|
||||
"translation*) na wejściu podawany jest tekst (na ogół pojedyncze\n",
|
||||
"zdanie) źródłowy (*source sentence*) $S = (u_1,\\dots,u_{|S|})$, celem\n",
|
||||
"jest uzyskanie tekstu docelowego (*target sentence*)\n",
|
||||
"$T=(w_1,\\dots,w_{|T|})$. Zakładamy, że $S$ jest tekstem w pewnym języku\n",
|
||||
"źródłowym (*source language*), zaś $T$ — w innym języku, języku\n",
|
||||
"docelowym (*target language*).\n",
|
||||
"\n",
|
||||
"Współczesne tłumaczenie maszynowe jest oparte na metodach\n",
|
||||
"statystycznych — system uczy się na podstawie obszernego zbioru\n",
|
||||
"odpowiadających sobie zdań w obu językach. Taki zbiór nazywamy\n",
|
||||
"korpusem równoległym (*parallel corpus*). Duży zbiór korpusów\n",
|
||||
"równoległych dla wielu języków można znaleźć na stronie projektu [OPUS]([https://opus.nlpl.eu/](https://opus.nlpl.eu/)).\n",
|
||||
"Zobaczmy na przykład fragment EUROPARL (protokoły Parlamentu Europejskiego):\n",
|
||||
"\n",
|
||||
" $ wget 'https://opus.nlpl.eu/download.php?f=Europarl/v8/moses/en-pl.txt.zip' -O en-pl.txt.zip\n",
|
||||
" $ unzip en-pl.txt.zip\n",
|
||||
" $ paste Europarl.en-pl.en Europarl.en-pl.pl | shuf -n 5\n",
|
||||
" The adoption of these amendments by the Committee on the Environment meant that we could place more emphasis on patients' rights to information, rather than make it an option for the pharmaceutical industries to provide that information.\tPrzyjęcie tych poprawek przez Komisję Ochrony Środowiska Naturalnego oznaczało, że mogliśmy położyć większy nacisk na prawo pacjentów do informacji, zamiast uczynić zeń możliwość, z której branża farmaceutyczna może skorzystać w celu dostarczenia informacji.\n",
|
||||
" I hope that the High Representative - who is not here today - will raise this episode with China and also with Nepal, whose own nascent democracy is kept afloat partly by EU taxpayers' money in the form of financial aid.\tMam nadzieję, że nieobecna dzisiaj wysoka przedstawiciel poruszy tę kwestię w rozmowach z Chinami, ale również z Nepalem, którego młoda demokracja funkcjonuje częściowo dzięki finansowej pomocy pochodzącej z pieniędzy podatników w UE.\n",
|
||||
" Immunity and privileges of Renato Brunetta (vote)\tWniosek o obronę immunitetu parlamentarnego Renata Brunetty (głosowanie)\n",
|
||||
" The 'new Member States' - actually, the name continues to be sort of conditional, making it easier to distinguish between the 'old' Member States and those that acceded to the EU after two enlargement rounds, owing to their particular historical background and perhaps the fact that they are poorer than the old ones.\"Nowe państwa członkowskie” - ta nazwa nadal ma w pewnym sensie charakter warunkowy i ułatwia rozróżnienie pomiędzy \"starszymi” państwami członkowskimi oraz tymi, które przystąpiły do UE po dwóch rundach rozszerzenia, które wyróżnia ich szczególna historia, a zapewne także fakt, że są uboższe, niż starsze państwa członkowskie.\n",
|
||||
" The number of armed attacks also rose by 200% overall.\tTakże liczba ataków zbrojnych wzrosła łącznie o 200 %.\n",
|
||||
"\n",
|
||||
"Zauważmy, że możemy taki (bi)tekst modelować po prostu traktując jako\n",
|
||||
"jeden ciągły tekst. Innymi słowy, nie modelujemy tekstu angielskiego ani polskiego,\n",
|
||||
"tylko angielsko-polską mieszankę, to znaczy uczymy model, który najpierw modeluje prawdopodobieństwo\n",
|
||||
"po stronie źródłowej (powiedzmy — angielskiej):\n",
|
||||
"\n",
|
||||
" The number of armed attacks also ?\n",
|
||||
"\n",
|
||||
"W momencie napotkania specjalnego tokenu końca zdania źródłowego (powiedzmy `<eoss>`) model\n",
|
||||
"powinien nauczyć się, że musi przełączyć się na modelowanie tekstu w języku docelowym (powiedzmy — polskim):\n",
|
||||
"\n",
|
||||
" The number of armed attacks also rose by 200% overall.<eoss>Także liczba ataków ?\n",
|
||||
"\n",
|
||||
"W czasie uczenia wykorzystujemy korpus równoległy traktując go po prostu jako zwykły ciągły tekst\n",
|
||||
"(dodajemy tylko specjalne tokeny końca zdania źródłowego i końca zdania docelowego).\n",
|
||||
"\n",
|
||||
"W fazie inferencji (w tłumaczeniu maszynowym tradycyjnie nazywaną\n",
|
||||
"**dekodowaniem**) zamieniamy nasz model języka w generator i podajemy\n",
|
||||
"tłumaczone zdanie jako prefiks, doklejając tylko token `<eoss>`.\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"##### Neuronowe modele języka jako translatory\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Jako że n-gramowego modelu języka ani modelu opartego na worku słów\n",
|
||||
"nie da się użyć w omawiany sposób w tłumaczeniu maszynowym\n",
|
||||
"(dlaczego?), jako pierwszych użyto w neuronowym tłumaczeniu maszynowym\n",
|
||||
"sieci LSTM, przy zastosowaniu omawianego wyżej sposobu.\n",
|
||||
"\n",
|
||||
"System tłumaczenia oparte na sieciach LSTM działały zaskakująco\n",
|
||||
"dobrze, zważywszy na to, że cała informacja o zdaniu źródłowym musi\n",
|
||||
"zostać skompresowana do wektora o stałym rozmiarze. (Dlaczego? W\n",
|
||||
"momencie osiągnięcia tokenu `<eoss>` cały stan sieci to kombinacja\n",
|
||||
"właściwego stanu $\\vec{s_i}$ i komórki pamięci $\\vec{c_i}$.)\n",
|
||||
"\n",
|
||||
"Neuronowe tłumaczenie oparte na sieciach LSTM działa względnie dobrze\n",
|
||||
"dla krótkich zdań, dla dłuższych rezultaty są gorsze — po prostu sieć\n",
|
||||
"nie jest w stanie skompresować w wektorze o stałej długości znaczenia\n",
|
||||
"całego zdania. Na początku rozwoju neuronowego tłumaczenia maszynowego\n",
|
||||
"opracowano kilka metod radzenia sobie z tym problemem (np. zaskakująco\n",
|
||||
"dobrze działa odwrócenie zdania źródłowego — siec LSTM łatwiej zacząć\n",
|
||||
"generować zdanie docelowe, jeśli niedawno „widziała” początek zdania\n",
|
||||
"źródłowego, przynajmniej dla pary języków o podobnym szyku).\n",
|
||||
"\n",
|
||||
"Najlepsze efekty dodało dodanie atencji do modelu LSTM\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"##### Atencja w sieciach rekurencyjnych\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Funkcję rekurencyjną można rozbudować o trzeci argument, w którym\n",
|
||||
"podany będzie wynik działania atencji $A'$ względem ostatniego wyrazu, tj.:\n",
|
||||
"\n",
|
||||
"$$A(w_1,\\dots,w_t) = R(A(w_1,\\dots,w_{t-1}), A'(w_1,\\dots,w_{t-1}), E(w_t)),$$\n",
|
||||
"\n",
|
||||
"W czasie tłumaczenia model może kierować swoją uwagę na wyrazy\n",
|
||||
"powiązane z aktualnie tłumaczonym fragmentem (zazwyczaj — po prostu odpowiedniki).\n",
|
||||
"\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.10.5"
|
||||
},
|
||||
"org": null
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 1
|
||||
}
|
@ -88,7 +88,7 @@ słowo odgadywane.
|
||||
*** Diagram
|
||||
|
||||
#+CAPTION: Atencja użyta w prostym neuronowym modelu języka
|
||||
[[./10_Atencja/simple-attention.drawio.png]]
|
||||
[[./13_Atencja/simple-attention.drawio.png]]
|
||||
|
||||
|
||||
** Atencja jako składnik sieci rekurencyjnej
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
Loading…
Reference in New Issue
Block a user