Word2vec Visualization

작년에 정부주도의 차세대 정보처리 연구사업(이하 차세정) 2019에 연구원으로서 참여했을 때

처음으로 받았던 과제이지만 당시에는 내 실력이 너무 부족했기 때문에

시도조차 할 수 없었다.

오랜만에 지도교수님을 뵈었을 때, 실력이 늘었으면 한 번 시도해보라는 말씀에 다시 과제를 잡게 되었다.

그런데 웬걸, 몇시간도 걸리지 않고 과제를 완료할 수 있었다.

과제관련 github

Van-Thuy Phi라는 사람이 만든 프로젝트로

word2vec model을 json으로 converting한 후 웹페이지에 tree구조로 시각화하는 것이 과제였다.

내가 받은 과제에서는 english-cosine-skipgram으로 생성된 모델들이 전처리가 되지 않은 채 들어있었다.

Regex (정규표현식)을 이용하여 전처리를 해준 뒤 바로 웹에 띄워보려고 했지만,

검색하려는 target 단어가 json 모델 안에 key값으로 등재되어 있지 않으면 출력되지 않는 문제가 발생했다.

1
2
3
4
5
6
7
8
9
10
if (!(nearest_words[i]["w"] in data)) {
if (i != (topn - 1)) {
flare += "]},";
}
else {
flare += "]}";
}
continue;
}
else {

이 부분을 해결하기 위해 frontend/js/index.js 파일에서 5개의 반복문 각각이 시작되기 직전에

위와 같은 if else 구문을 추가하여 검색하려는 단어가 없으면 json format만 채워주고,

검색할 단어가 있을 때만 json을 parsing하는 코드가 돌아가도록 구성했다.

코드 전체 보기
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

var margin = {top: 20, right: 120, bottom: 20, left: 120},
width = 960 - margin.right - margin.left,
height = 800 - margin.top - margin.bottom;

var i = 0,
duration = 750,
root;

var tree = d3.layout.tree()
.size([height, width]);

var diagonal = d3.svg.diagonal()
.projection(function(d) { return [d.y, d.x]; });

var svg = d3.select("body").append("svg")
//.attr("width", width + margin.right + margin.left)
.attr("width", '100%')
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");


document.getElementById("submit").onclick = function() {
svg.selectAll("*").remove();
var word = document.getElementById("word").value;
var topn = document.getElementById("topn").value;
//console.log(word);
if (word == "")
alert("Please enter a word!");
else
visualize(word, topn);

}


function createFlare(word, topn, data) {

/*flare = {
"name": "student",
"children": [
{"name": "graduate"},
{"name": "hosei"},
{"name": "daigaku"},
{"name": "college"},
{"name": "rikkyo"},
{"name": "graduated"},
{"name": "doctorate"},
{"name": "cambridge"},
{"name": "sophia"},
{"name": "doctoral"}
]
};*/

topn = topn;

flare = "{";

word = word;

// Check whether word is in list?
if (!(word in data)) {
console.log("Word Not Found!");
return 0;
}

flare += "\"name\": \"" + word + "\",";
flare += "\"children\": [";

nearest_words = data[word];

for (i = 0; i < topn; i++) {
flare += "{";

flare += "\"name\": \"" + nearest_words[i]["w"] + "\","; //Before loop 1

// Loop 1

flare += "\"children\": [";

if (!(nearest_words[i]["w"] in data)) {
if (i != (topn - 1)) {
flare += "]},";
}
else {
flare += "]}";
}
continue;
}
else {
nearest_words_1 = data[nearest_words[i]["w"]];

for (i1 = 0; i1 < topn; i1++) {
flare += "{";

flare += "\"name\": \"" + nearest_words_1[i1]["w"] + "\","; //Before loop 2

// Loop 2
flare += "\"children\": [";

if (!(nearest_words_1[i1]["w"] in data)) {
if (i1 != (topn - 1)) {
flare += "]},";
}
else {
flare += "]}";
}
continue;
}
else {
nearest_words_2 = data[nearest_words_1[i1]["w"]];

for (i2 = 0; i2 < topn; i2++) {
flare += "{";

flare += "\"name\": \"" + nearest_words_2[i2]["w"] + "\","; //Before loop 3

// Loop 3
flare += "\"children\": [";

if (!(nearest_words_2[i2]["w"] in data)) {
if (i2 != (topn - 1)) {
flare += "]},";
}
else {
flare += "]}";
}
continue;
}
else {
nearest_words_3 = data[nearest_words_2[i2]["w"]];

for (i3 = 0; i3 < topn; i3++) {
flare += "{";

flare += "\"name\": \"" + nearest_words_3[i3]["w"] + "\","; //Before loop 4

// Loop 4
flare += "\"children\": [";

if (!(nearest_words_3[i3]["w"] in data)) {
if (i3 != (topn - 1)) {
flare += "]},";
}
else {
flare += "]}";
}
continue;
}
else {
nearest_words_4 = data[nearest_words_3[i3]["w"]];

for (i4 = 0; i4 < topn; i4++) {
flare += "{";

flare += "\"name\": \"" + nearest_words_4[i4]["w"] + "\""; //Before loop ?
// If this is the final layer --> modify (delete ",")
if (i4 != (topn - 1)) {
flare += "},";
}
else {
flare += "}";
}
}
}
flare += "]";
// End Loop 3

if (i3 != (topn - 1)) {
flare += "},";
}
else {
flare += "}";
}
}
}
flare += "]";
// End Loop 3

if (i2 != (topn - 1)) {
flare += "},";
}
else {
flare += "}";
}
}
}
flare += "]";
// End Loop 2

if (i1 != (topn - 1)) {
flare += "},";
}
else {
flare += "}";
}
}
}
flare += "]";
// End Loop 1

if (i != (topn - 1)) {
flare += "},";
}
else {
flare += "}";
}

}

flare += "]";

flare += "}";

return JSON.parse(flare);
}


function visualize(word, topn) {

word = word;
topn = topn;

if (document.getElementById("language").elements["language"].value == "English") {
if (document.getElementById("metric").value == "Cosine") {
if (document.getElementById("model").value == "Skipgram")
data_file = "data/en_data_cosine_skipgram-gb21-Q.json";
else
data_file = "data/en_data_cosine_cbow.json";
}
else {
if (document.getElementById("model").value == "Skipgram")
data_file = "data/en_data_euclidean_skipgram.json";
else
data_file = "data/en_data_euclidean_cbow.json";
}
}
else {
if (document.getElementById("metric").value == "Cosine") {
if (document.getElementById("model").value == "Skipgram")
data_file = "data/ja_data_cosine_skipgram.json";
else
data_file = "data/ja_data_cosine_cbow.json";
}
else {
if (document.getElementById("model").value == "Skipgram")
data_file = "data/ja_data_euclidean_skipgram.json";
else
data_file = "data/ja_data_euclidean_cbow.json";
}
}

d3.json(data_file, function(error, json) {
if (error) throw error;

json_data = json;

root = createFlare(word, topn, json_data);

// Word Not Found
if (root == 0) {
alert("Word Not Found!");
return;
}

root.x0 = height / 2;
root.y0 = 0;

function collapse(d) {
if (d.children) {
d._children = d.children;
d._children.forEach(collapse);
d.children = null;
}
}

root.children.forEach(collapse);
update(root);

});

d3.select(self.frameElement).style("height", "800px");
}

function update(source) {

// Compute the new tree layout.
var nodes = tree.nodes(root).reverse(),
links = tree.links(nodes);

// Normalize for fixed-depth.
nodes.forEach(function(d) { d.y = d.depth * 180; });

// Update the nodes…
var node = svg.selectAll("g.node")
.data(nodes, function(d) { return d.id || (d.id = ++i); });

// Enter any new nodes at the parent's previous position.
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; })
.on("click", click);

nodeEnter.append("circle")
.attr("r", 1e-6)
.style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });

nodeEnter.append("text")
.attr("x", function(d) { return d.children || d._children ? -10 : 10; })
.attr("dy", ".35em")
.attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
.text(function(d) { return d.name; })
.style("fill-opacity", 1e-6);

// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });

nodeUpdate.select("circle")
.attr("r", 4.5)
.style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });

nodeUpdate.select("text")
.style("fill-opacity", 1);

// Transition exiting nodes to the parent's new position.
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) { return "translate(" + source.y + "," + source.x + ")"; })
.remove();

nodeExit.select("circle")
.attr("r", 1e-6);

nodeExit.select("text")
.style("fill-opacity", 1e-6);

// Update the links…
var link = svg.selectAll("path.link")
.data(links, function(d) { return d.target.id; });

// Enter any new links at the parent's previous position.
link.enter().insert("path", "g")
.attr("class", "link")
.attr("d", function(d) {
var o = {x: source.x0, y: source.y0};
return diagonal({source: o, target: o});
});

// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", diagonal);

// Transition exiting nodes to the parent's new position.
link.exit().transition()
.duration(duration)
.attr("d", function(d) {
var o = {x: source.x, y: source.y};
return diagonal({source: o, target: o});
})
.remove();

// Stash the old positions for transition.
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
}

// Toggle children on click.
function click(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
}

word2vec 모델이 tree 구조로 완벽하게 시각화 된 것을 확인할 수 있었다.

코퍼스의 규모가 작아서 타겟 단어들이 key값으로 들어있지 않다 하더라도 검색이 되는 것을 확인할 수 있다.

(노드에서 파란색 동그라미가 흰색이 되었지만 tree구조로 시각화되지 않으면 검색이 안되는 단어들임)

애초에 모델을 잘 생성하는 게 좋긴 한가보다.

그래도 확장성과 관련하여 여러 코퍼스 모델에 적용하기도 용이하고, NLP관련 연구에 많은 도움이 될 것 같다.

javascript 언어를 사용해본 적도 없는데 이렇게 과제를 해결한 걸 보면

확실히 실력이 늘긴 늘었나보다.

뿌듯하구만.

Author

Yohan Lee

Posted on

2020-06-23

Updated on

2021-08-22

Licensed under

댓글