<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>dong-dong_e4.log</title>
        <link>https://velog.io/</link>
        <description>관성을 붙이자</description>
        <lastBuildDate>Mon, 01 Jan 2024 22:24:16 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>dong-dong_e4.log</title>
            <url>https://velog.velcdn.com/images/dong-dong_e4/profile/a319c87d-2b4c-40fe-b35f-d0521832c038/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. dong-dong_e4.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dong-dong_e4" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[GDSC X codeit project track] 12월 회고]]></title>
            <link>https://velog.io/@dong-dong_e4/GDSC-X-codeit-project-track-12%EC%9B%94-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@dong-dong_e4/GDSC-X-codeit-project-track-12%EC%9B%94-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Mon, 01 Jan 2024 22:24:16 GMT</pubDate>
            <description><![CDATA[<p>벌써 codeit 프로젝트에 참가한 지 두달째가 되었다. 이제 본격적인 개발 과정이 시작될 예정이고 많이 기대가 된다.</p>
<blockquote>
<p>한 일들</p>
</blockquote>
<p>11월 말부터 12월 초까지는 시험기간이라 사실상 django 공부를 많이 하지 못했다. 그래서 종강하고도 시험기간에 밀렸던 공부들, 그리고 11월에 밀렸던 진도까지 다 따라잡느라고 놀지를 못했다. 일단 5주차와 6주차, 그리고 종강 후에 진행된 진도의 9주차까지 한꺼번에 하느라고 고생을 좀 했다. 하지만 그래도 프로젝트를 앞두고 진도를 다 끝내서 안심은 된다.</p>
<blockquote>
<p>총평</p>
</blockquote>
<p>학교 공부와 병행해야 했던 11월보다는 덜 빡센 한 달이었다. 이제 본격적으로 시작될 실제 개발 과정을 앞두고 프로젝트 경험을 쌓을 것에 대한 기대감, 그리고 어떤 난관에 부딪히게 될까 하는 것에 대한 불안감이 공존하는 상태다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[GDSC X codeit project track] 11월 회고]]></title>
            <link>https://velog.io/@dong-dong_e4/GDSC-X-codeit-project-track-11%EC%9B%94-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@dong-dong_e4/GDSC-X-codeit-project-track-11%EC%9B%94-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Thu, 30 Nov 2023 17:22:47 GMT</pubDate>
            <description><![CDATA[<p>GDSC와 codeit에서 주관하는 프로젝트 트랙에 참가한 지도 벌써 한 달이 넘어간다. 프로젝트에 참가한 후 처음으로 쓰는 MIL이다. 이 글에 지난 한 달 간 느낀 점, 그리고 개인적으로 반성할 점들도... 한번 써 보려고 한다 ㅋㅋㅋㅋㅋ</p>
<blockquote>
<p>2주차 코어타임</p>
</blockquote>
<p>다들 참여하고 싶어하는 프로젝트에 선발되어 정말 기쁜 마음이었고 들뜬 기분으로 참가한 첫 코어타임이었다. 첫 주에 생각보다 많은 공부량에 진도를 끝까지 못 듣고 갔던 기억이 난다. 코어타임 내내 다 듣지 못한 강의를 듣고 집에 돌아가서도 몇 시간 더 들어서 끝냈지만 다음주에는 잘 해갈 수 있겠지 하는 생각을 했었다. 하지만 그때 알았어야 했다. 그 주가 가장 쉬운 주였다는 것을...</p>
<blockquote>
<p>3주차 코어타임</p>
</blockquote>
<p>학교 프로젝트 수업 중간 발표가 있어 한 주 내내 codeit 강의를 못 들었었다. 그래도 짬짬이 내서 강의 들었어야 했는데 그 때는 시간이 없다는 핑계로 나와의 약속을 안 지켰던 것 같다. 결국 과제를 제때 못 내고 말았다...</p>
<blockquote>
<p>4주차 코어타임</p>
</blockquote>
<p>3주차 밀린 진도 따라잡는데도 버거워 헉헉대는데 4주차 진도는 너무 버거웠다. 하지만 내가 쌓은 업보니 거의 하루에 4-5시간씩 투자하면서 따라가려고 노력했던 것 같다. 하지만 아직도 진도를 따라잡기에는 벅찼다. 그리고 학교 프로젝트도 계속 진행해야 했다.</p>
<blockquote>
<p>5주차 코어타임</p>
</blockquote>
<p>결국 과제는 4주차 분량만 내고 말았다. 이번 주는 특히 학교 프로젝트 최종 발표가 있어서 시간을 내기가 더 어려웠다. 이번 주말에 시험 기간 전 마지막 코어타임이 있어 그 전까지 진도를 따라잡고 싶었는데 결국 5주차 과제를 내는 것만으로 만족해야 할 것 같다.</p>
<blockquote>
<p>총평</p>
</blockquote>
<p>사실 내가 생각한 것보다 공부량이 많았다. 그래서 한번 진도가 밀리니 답이 없었다. 점점 급하게 진도에 쫒기니 과제를 위한 공부를 하고 있다는 생각이 들었다. 이번 주까지 5주차 과제를 마저 내고 시험 끝나고 밀린 강의를 청산할 생각이다. 이번 달 프로젝트에 참여하면서 느꼈던 것은 내가 하루 꼭 지켜야 하는 공부시간을 지키지 않았을 때 벌어지는 일이었다. 시험이 끝나면 당분간 codeit 강의만 들으면서 밀린 진도를 따라잡고, 프로젝트에 django 개발자로 참여할 수 있도록 실력을 끌어올릴 생각이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[crnn 코드 뜯어보기(2)]]></title>
            <link>https://velog.io/@dong-dong_e4/crnn-%EC%BD%94%EB%93%9C-%EB%9C%AF%EC%96%B4%EB%B3%B4%EA%B8%B02</link>
            <guid>https://velog.io/@dong-dong_e4/crnn-%EC%BD%94%EB%93%9C-%EB%9C%AF%EC%96%B4%EB%B3%B4%EA%B8%B02</guid>
            <pubDate>Thu, 09 Nov 2023 12:49:53 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@dong-dong_e4/crnn-%EC%BD%94%EB%93%9C-%EB%9C%AF%EC%96%B4%EB%B3%B4%EA%B8%B0">앞 내용</a>
<a href="https://github.com/Belval/CRNN/blob/master/CRNN/crnn.py">출처</a></p>
<pre><code class="language-python">def crnn(self, max_width):
        def BidirectionnalRNN(inputs, seq_len):
            &quot;&quot;&quot;
                Bidirectionnal LSTM Recurrent Neural Network part
            &quot;&quot;&quot;

            with tf.variable_scope(None, default_name=&quot;bidirectional-rnn-1&quot;):
                # Forward
                lstm_fw_cell_1 = rnn.BasicLSTMCell(256)
                # Backward
                lstm_bw_cell_1 = rnn.BasicLSTMCell(256)

                inter_output, _ = tf.nn.bidirectional_dynamic_rnn(
                    lstm_fw_cell_1, lstm_bw_cell_1, inputs, seq_len, dtype=tf.float32
                )

                inter_output = tf.concat(inter_output, 2)

            with tf.variable_scope(None, default_name=&quot;bidirectional-rnn-2&quot;):
                # Forward
                lstm_fw_cell_2 = rnn.BasicLSTMCell(256)
                # Backward
                lstm_bw_cell_2 = rnn.BasicLSTMCell(256)

                outputs, _ = tf.nn.bidirectional_dynamic_rnn(
                    lstm_fw_cell_2,
                    lstm_bw_cell_2,
                    inter_output,
                    seq_len,
                    dtype=tf.float32,
                )

                outputs = tf.concat(outputs, 2)

            return outputs

        def CNN(inputs):
            &quot;&quot;&quot;
                Convolutionnal Neural Network part
            &quot;&quot;&quot;

            # 64 / 3 x 3 / 1 / 1
            conv1 = tf.layers.conv2d(
                inputs=inputs,
                filters=64,
                kernel_size=(3, 3),
                padding=&quot;same&quot;,
                activation=tf.nn.relu,
            )

            # 2 x 2 / 1
            pool1 = tf.layers.max_pooling2d(inputs=conv1, pool_size=[2, 2], strides=2)

            # 128 / 3 x 3 / 1 / 1
            conv2 = tf.layers.conv2d(
                inputs=pool1,
                filters=128,
                kernel_size=(3, 3),
                padding=&quot;same&quot;,
                activation=tf.nn.relu,
            )

            # 2 x 2 / 1
            pool2 = tf.layers.max_pooling2d(inputs=conv2, pool_size=[2, 2], strides=2)

            # 256 / 3 x 3 / 1 / 1
            conv3 = tf.layers.conv2d(
                inputs=pool2,
                filters=256,
                kernel_size=(3, 3),
                padding=&quot;same&quot;,
                activation=tf.nn.relu,
            )

            # Batch normalization layer
            bnorm1 = tf.layers.batch_normalization(conv3)

            # 256 / 3 x 3 / 1 / 1
            conv4 = tf.layers.conv2d(
                inputs=bnorm1,
                filters=256,
                kernel_size=(3, 3),
                padding=&quot;same&quot;,
                activation=tf.nn.relu,
            )

            # 1 x 2 / 1
            pool3 = tf.layers.max_pooling2d(
                inputs=conv4, pool_size=[2, 2], strides=[1, 2], padding=&quot;same&quot;
            )

            # 512 / 3 x 3 / 1 / 1
            conv5 = tf.layers.conv2d(
                inputs=pool3,
                filters=512,
                kernel_size=(3, 3),
                padding=&quot;same&quot;,
                activation=tf.nn.relu,
            )

            # Batch normalization layer
            bnorm2 = tf.layers.batch_normalization(conv5)

            # 512 / 3 x 3 / 1 / 1
            conv6 = tf.layers.conv2d(
                inputs=bnorm2,
                filters=512,
                kernel_size=(3, 3),
                padding=&quot;same&quot;,
                activation=tf.nn.relu,
            )

            # 1 x 2 / 2
            pool4 = tf.layers.max_pooling2d(
                inputs=conv6, pool_size=[2, 2], strides=[1, 2], padding=&quot;same&quot;
            )

            # 512 / 2 x 2 / 1 / 0
            conv7 = tf.layers.conv2d(
                inputs=pool4,
                filters=512,
                kernel_size=(2, 2),
                padding=&quot;valid&quot;,
                activation=tf.nn.relu,
            )

            return conv7

        batch_size = None
        inputs = tf.placeholder(
            tf.float32, [batch_size, max_width, 32, 1], name=&quot;input&quot;
        )

        # Our target output
        targets = tf.sparse_placeholder(tf.int32, name=&quot;targets&quot;)

        # The length of the sequence
        seq_len = tf.placeholder(tf.int32, [None], name=&quot;seq_len&quot;)

        cnn_output = CNN(inputs)
        reshaped_cnn_output = tf.squeeze(cnn_output, [2])
        max_char_count = cnn_output.get_shape().as_list()[1]

        crnn_model = BidirectionnalRNN(reshaped_cnn_output, seq_len)

        logits = tf.reshape(crnn_model, [-1, 512])
        W = tf.Variable(
            tf.truncated_normal([512, self.NUM_CLASSES], stddev=0.1), name=&quot;W&quot;
        )
        b = tf.Variable(tf.constant(0.0, shape=[self.NUM_CLASSES]), name=&quot;b&quot;)

        logits = tf.matmul(logits, W) + b
        logits = tf.reshape(
            logits, [tf.shape(cnn_output)[0], max_char_count, self.NUM_CLASSES]
        )

        # Final layer, the output of the BLSTM
        logits = tf.transpose(logits, (1, 0, 2))

        # Loss and cost calculation
        loss = tf.nn.ctc_loss(
            targets, logits, seq_len, ignore_longer_outputs_than_inputs=True
        )

        cost = tf.reduce_mean(loss)

        # Training step
        optimizer = tf.train.AdamOptimizer(learning_rate=0.0001).minimize(cost)

        # The decoded answer
        decoded, log_prob = tf.nn.ctc_beam_search_decoder(
            logits, seq_len, merge_repeated=False
        )
        dense_decoded = tf.sparse_tensor_to_dense(
            decoded[0], default_value=-1, name=&quot;dense_decoded&quot;
        )

        # The error rate
        acc = tf.reduce_mean(tf.edit_distance(tf.cast(decoded[0], tf.int32), targets))

        init = tf.global_variables_initializer()

        return (
            inputs,
            targets,
            seq_len,
            logits,
            dense_decoded,
            optimizer,
            acc,
            cost,
            max_char_count,
            init,
        )</code></pre>
<p>crnn 함수는 max_width를 매개변수로 받고 nested function으로 BidirectionnalRNN(inputs, seq_len)과 CNN(inputs)을 가지고 있다.</p>
<p>우선 BidirectionnalRNN 부터 살펴보자.</p>
<p>BidirectionnalRNN은 매개변수로 inputs와 seq_len을 가지고 있고 with문으로 짜여진 두 개의 <strong>변수 스코프</strong>로 이루어져 있다.</p>
<h4 id="변수-스코프란">변수 스코프란</h4>
<p>변수 스코프(Variable Scope)는 텐서플로에서 변수들을 조직화하고 관리하는 방법 중 하나이다. 변수 스코프를 사용하면 모델 내에서 변수들을 계층적으로 구성하고, 변수의 이름 충돌을 방지하며, 코드를 더 읽기 쉽게 만들 수 있다.</p>
<p>우리가 살펴보고 있는 BidirectionnalRNN의 코드를 통해 변수 스코프를 쓰는 방법을 자세히 알아보자.</p>
<pre><code class="language-python">with tf.variable_scope(None, default_name=&quot;bidirectional-rnn-1&quot;):
                # Forward
                lstm_fw_cell_1 = rnn.BasicLSTMCell(256)
                # Backward
                lstm_bw_cell_1 = rnn.BasicLSTMCell(256)

                inter_output, _ = tf.nn.bidirectional_dynamic_rnn(
                    lstm_fw_cell_1, lstm_bw_cell_1, inputs, seq_len, dtype=tf.float32
                )

                inter_output = tf.concat(inter_output, 2)

with tf.variable_scope(None, default_name=&quot;bidirectional-rnn-2&quot;):
                # Forward
                lstm_fw_cell_2 = rnn.BasicLSTMCell(256)
                # Backward
                lstm_bw_cell_2 = rnn.BasicLSTMCell(256)

                outputs, _ = tf.nn.bidirectional_dynamic_rnn(
                    lstm_fw_cell_2,
                    lstm_bw_cell_2,
                    inter_output,
                    seq_len,
                    dtype=tf.float32,
                )

                outputs = tf.concat(outputs, 2)
</code></pre>
<p>변수 스코프는 default_name으로 각각 &quot;bidirectional-rnn-1&quot;, &quot;bidirectional-rnn-2&quot;를 가지고 있다. 
따라서 이름이 지정되지 않았을 때 lstm_fw_cell_1, lstm_bw_cell_1, inter_output은 bidirectional-rnn-1 변수 스코프 안에서, lstm_fw_cell_2, lstm_bw_cell_2와 outputs는 bidirectional-rnn-2 변수 스코프 안에서 생성되고 변수 스코프의 이름을 접두어로 가지게 된다.</p>
<p>변수 스코프의 개념을 이해했으니 이제 변수 스코프 내에 선언된 변수들에 대해 자세히 알아보자.</p>
<blockquote>
<p>lstm_fw_cell_1: rnn.BasicLSTMCell(256)이 256개의 유닛을 가진 전방향 LSTM 셀을 생성하여 저장한다.
lstm_bw_cell_1: rnn.BasicLSTMCell(256)이 256개의 유닛을 가진 후방향 LSTM 셀을 생성하여 저장한다.</p>
</blockquote>
<p>여기서 lstm이라는 생소한 용어가 나와 당황했는데, 
<a href="https://yeong-jin-data-blog.tistory.com/entry/LSTM">LSTM, Bidirectional LSTM</a>
다음의 블로그 글을 보고 해결했다. LSTM은 RNN의 한 종류일 뿐이었다. RNN은 많은 시간이 지나면 이전의 input을 잊어버린다는 단점이 있었는데, 이것을 &#39;RNN의 장기의존문제&#39;라고 부른다고 한다. 여기서 LSTM이라는 모델은 <code>기억 셀(memory cell)</code>을 추가하면서 이 문제를 해결했다. 우리가 지금 분석하고 있는 이 코드에서도 LSTM을 사용하여 RNN을 구현해 주려고 하는 것 같다.</p>
<p>하지만 LSTM을 직접 구현해주는 형태가 아니라, 코드의 첫부분에서 <code>from tensorflow.contrib import rnn</code> 문으로 tensorflow의 contrib라는 라이브러리에서 rnn을 import 해오는 식으로 구현하였기에 모든 모델을 직접 구현하려고 하는 우리 팀의 입장에서는 그대로 쓰기 곤란하다는 생각이 들었다. 그리고 우리가 사용하려고 하는 데이터셋 특성상 rnn의 장기의존문제가 발생할 정도로 input이 길지는 않을 것 같아 그냥 상대적으로 간단해 보이는 rnn의 구현이 더 좋을 것 같기도 하다. 물론 구현해봐야 알겠지만 말이다.</p>
<blockquote>
<p>outputs, _ = tf.nn.bidirectional_dynamic_rnn(
                    lstm_fw_cell_2,
                    lstm_bw_cell_2,
                    inter_output,
                    seq_len,
                    dtype=tf.float32,
                )</p>
</blockquote>
<p>가장 이해하기 어려웠던 부분이었다. <code>outputs,</code> 옆에 붙은 <code>_</code>의 존재가 이해되지 않았다. 이 부분은 알고 보니 반환값이 2개인 tf.nn.bidirectional_dynamic_rnn 함수의 특성 때문이었다. 이 코드에서는 tf.nn.bidirectional_dynamic_rnn가 반환하는 두 개의 텐서 중 첫 번째만 사용하기로 하고 두 번째 텐서는 무시하기 위해 _ 를 사용했다.
<img src="https://velog.velcdn.com/images/dong-dong_e4/post/3306e694-6f37-46fe-a184-0a0588b2c8b1/image.png" alt=""></p>
<pre><code class="language-python">batch_size = None
        inputs = tf.placeholder(
            tf.float32, [batch_size, max_width, 32, 1], name=&quot;input&quot;
        )</code></pre>
<p>batch size는 한 interation 안에 돌아갈 훈련 데이터의 양이다. <code>batch_size = None</code>으로 설정해줬다는 것은 batch size를 고정해주지 않았다는 것을 의미한다. 이렇게 하면 batch size는 나중에 모델이 실행될 때 정해줘야 한다.</p>
<h3 id="tfplaceholder">tf.placeholder</h3>
<p><code>tf.placeholder</code>함수는 자료형의 일종이다. 일반적인 자료형은 아니고 다른 tensor를 placeholder에 매핑시키는 것이다. &#39;placeholder&#39;의 뜻이 &#39;자리 표시자&#39;라는 것을 생각해 보면 감이 좀 온다.
<a href="https://gdyoon.tistory.com/5">placeholder 문법</a>
다음 블로그를 참고해 이해하였다.</p>
<p>첫번째 매개변수 <code>tf.float32</code>는 placeholder에 들어갈 매개변수, 두번째 매개변수 <code>[batch_size, max_width, 32, 1]</code>는 placeholder의 형태, 그리고 세번째 매개변수 <code>name=&quot;input&quot;</code>는 placeholder의 이름을 정해준다.</p>
<pre><code class="language-python">          #Our target output
        targets = tf.sparse_placeholder(tf.int32, name=&quot;targets&quot;)

        # The length of the sequence
        seq_len = tf.placeholder(tf.int32, [None], name=&quot;seq_len&quot;)</code></pre>
<h3 id="cnn-출력값을-bidirectionnalrnn에-넣어주기">cnn 출력값을 BidirectionnalRNN에 넣어주기</h3>
<p><code>targets = tf.sparse_placeholder(tf.int32, name=&quot;targets&quot;)</code>은 목표 출력에 대한 sparse_placeholder를 생성한다. tf.sparse_placeholder 함수는 sparse tensor를 처리하는 데 사용됩니다. 희소 텐서의 값의 데이터 유형은 tf.int32로 설정되어 있다.</p>
<pre><code class="language-python">cnn_output = CNN(inputs)
reshaped_cnn_output = tf.squeeze(cnn_output, [2])
max_char_count = cnn_output.get_shape().as_list()[1]</code></pre>
<p><code>cnn_output = CNN(inputs)</code>: cnn에 의해 처리된 결과를 cnn_output에 저장한다.
<code>reshaped_cnn_output = tf.squeeze(cnn_output, [2])</code>: tf.squeeze 함수는 특정 차원에서 크기가 1인 차원을 제거하여 데이터를 압축하는 역할을 한다. 이 코드에서는 cnn_output에서 세 번째 차원(<code>[2]</code>)에서 크기가 1인 차원을 제거하였다.
<code>max_char_count = cnn_output.get_shape().as_list()[1]</code>: cnn_output의 형태(shape) 정보를 사용하여 출력 데이터의 두 번째 차원의 크기를 가져온다(이 값은 최대 문자 수를 나타낸다).
코드에서는 get_shape().as_list()를 사용하여 텐서의 형태를 리스트로 변환하고, 그 중에서 두 번째 요소를 선택하여 최대 문자 수를 얻는다.</p>
<p>종합하면, 이 코드는 CNN을 사용하여 입력 데이터를 처리하고, 그 결과를 압축하여 다루기 쉬운 형태로 만든다. 그리고 최대 문자 수를 max_char_count 변수에 저장한다.</p>
<pre><code class="language-python">crnn_model = BidirectionnalRNN(reshaped_cnn_output, seq_len)</code></pre>
<p>BidirectionnalRNN에 처리된 CNN의 출력과 시퀀스 길이를 넣어 초기화해 crnn_model에 넣는다.</p>
<h3 id="bi-rnn-출력의-최종-분류">Bi-RNN 출력의 최종 분류</h3>
<pre><code class="language-python">crnn_model = BidirectionnalRNN(reshaped_cnn_output, seq_len) 
#앞에서 살펴본 코드. crnn_model이 Bi-RNN의 출력을 받는다.

logits = tf.reshape(crnn_model, [-1, 512])
W = tf.Variable(
    tf.truncated_normal([512, self.NUM_CLASSES], stddev=0.1), name=&quot;W&quot;
) # 가중치 설정
b = tf.Variable(tf.constant(0.0, shape=[self.NUM_CLASSES]), name=&quot;b&quot;) #편향 설정</code></pre>
<p><code>tf.reshape(crnn_model, [-1, 512])</code>: tf.reshape 함수는 crnn_model에 담긴 Bi-RNN의 출력값을 2D 텐서로 변환한다. 변환된 텐서는 logits에 저장된다.
<code>W = tf.Variable(
    tf.truncated_normal([512, self.NUM_CLASSES], stddev=0.1), name=&quot;W&quot;
)</code>: 신경망의 가중치 설정
<code>b = tf.Variable(tf.constant(0.0, shape=[self.NUM_CLASSES]), name=&quot;b&quot;)</code>: 신경망의 편향 설정</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[crnn 코드 뜯어보기(1)]]></title>
            <link>https://velog.io/@dong-dong_e4/crnn-%EC%BD%94%EB%93%9C-%EB%9C%AF%EC%96%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@dong-dong_e4/crnn-%EC%BD%94%EB%93%9C-%EB%9C%AF%EC%96%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Wed, 08 Nov 2023 22:39:20 GMT</pubDate>
            <description><![CDATA[<p>이 글은 머신러닝을 1도 모르는 한 불쌍한 대학생이 학부에서 텀 프로젝트를 해내기 위해 논문 코드를 하나하나 뜯어보는 글이다. 배우면서 쓰는 글이다 보니 정확성이 많이 떨어질 수 있음을 양해 바란다.
주제는 crnn이라는 모델을 활용한 ocr 구현이다.
<a href="https://github.com/Belval/CRNN/blob/master/CRNN/crnn.py">출처</a></p>
<pre><code class="language-python">import os
import time
import numpy as np
import tensorflow as tf
from scipy.misc import imread, imresize, imsave
from tensorflow.contrib import rnn

from data_manager import DataManager
from utils import (
    sparse_tuple_from,
    resize_image,
    label_to_array,
    ground_truth_to_word,
    levenshtein,
)

os.environ[&quot;TF_CPP_MIN_LOG_LEVEL&quot;] = &quot;3&quot;

class CRNN(object):
    def __init__(
        self,
        batch_size, # 배치 사이즈 지정
        model_path, # 파라미터와 체크포인트 파일 저장, 로드할 디렉토리 경로 지정
        examples_path, # 학습 데이터가 있는 디렉토리 경로 지정
        max_image_width, #입력 이미지 최대 너비 지정
        train_test_ratio, # 학습, 테스트 데이터 나누는 비율 설정
        restore, # 모델을 이전에 학습한 상태로 복원할지 여부 결정 플래그
        char_set_string, # 모델이 인식해야 할 문자 집합 지정
        use_trdg, #True 또는 False 값을 가지며, 텍스트 데이터 증강 기술인 TextRecognitionDataGenerator(TRDG)를 사용할지 여부를 결정
        language, # 모델에서 사용할 언어를 지정
    ):
        self.step = 0
        self.CHAR_VECTOR = char_set_string # CHAR_VECTOR: 모델에서 인식해야 하는 문자 집합을 나타내는 문자
        self.NUM_CLASSES = len(self.CHAR_VECTOR) + 1 # NUM_CLASSES: 문자 집합에 포함된 문자 수에 1을 더한 값. 모델의 출력 클래스 수를 나타냄

        print(&quot;CHAR_VECTOR {}&quot;.format(self.CHAR_VECTOR))
        print(&quot;NUM_CLASSES {}&quot;.format(self.NUM_CLASSES))

        self.model_path = model_path #model_path: 모델 파일과 체크포인트 파일이 저장되거나 로드될 위치 지정
        self.save_path = os.path.join(model_path, &quot;ckp&quot;) # 체크포인트 파일이 저장될 위치 지정.
        # os.path.join(): 파일 시스템의 경로를 조인하여 새로운 경로를 만들어내는 함수

        self.restore = restore

        self.training_name = str(int(time.time())) # 모델 이름 지어주기
        self.session = tf.Session() # tf의 session 함수 인스턴스 생성

        # Building graph
        with self.session.as_default():
            (
                self.inputs, # 입력 데이터
                self.targets, # 타겟 데이터
                self.seq_len, 
                self.logits, # 출력
                self.decoded, # 디코딩된 결과
                self.optimizer, # 최적화 방법
                self.acc, # 정확도
                self.cost, # 손실
                self.max_char_count, # 최대 문자 수
                self.init, # 초기화 연산
            ) = self.crnn(max_image_width)
            self.init.run()

        with self.session.as_default():
            self.saver = tf.train.Saver(tf.global_variables(), max_to_keep=10)
            # Loading last save if needed
            if self.restore:
                print(&quot;Restoring&quot;)
                ckpt = tf.train.latest_checkpoint(self.model_path)
                if ckpt:
                    print(&quot;Checkpoint is valid&quot;)
                    self.step = int(ckpt.split(&quot;-&quot;)[1])
                    self.saver.restore(self.session, ckpt)

        # Creating data_manager
          self.data_manager = DataManager(
            batch_size,
            model_path,
            examples_path,
            max_image_width,
            train_test_ratio,
            self.max_char_count,
            self.CHAR_VECTOR,
            use_trdg,
            language,
        )</code></pre>
<p>다음 코드를 하나하나 뜯어보자.</p>
<blockquote>
<p>print(&quot;CHAR_VECTOR {}&quot;.format(self.CHAR_VECTOR))
print(&quot;NUM_CLASSES {}&quot;.format(self.NUM_CLASSES))</p>
</blockquote>
<p>우선 이 프린트 함수의 구조를 살펴보면:
&quot;NUM_CLASSES {}&quot; 부분은 출력 문자열의 형식을 정의한다. {}에는 나중에 들어갈 변수의 위치를 표시해준다.
.format(self.NUM_CLASSES) 부분은 중괄호 {}에 self.NUM_CLASSES 변수의 값을 삽입한다.</p>
<p>이 코드의 출력값은 다음과 같을 것이다</p>
<blockquote>
<p>CHAR_VECTOR ABCDEFGHI
NUM_CLASSES 10</p>
</blockquote>
<p>NUM_CLASSES는 CHAR_VECTOR의 길이에 1을 더한 값이므로 출력은 10이 된다.
다음 코드를 보자.</p>
<blockquote>
<p>self.model_path = model_path
        self.save_path = os.path.join(model_path, &quot;ckp&quot;)</p>
</blockquote>
<p>self.model_path와 self.save_path를 초기화한다.</p>
<p>self.model_path는 <strong>모델 파일과 체크포인트 파일을 저장하거나 로드할 디렉토리 경로를 나타낸다.</strong></p>
<p>os.path.join() 함수는 파일 시스템 경로를 조인하고 새 경로를 생성하는 데 사용된다. 여기서는 model_path와 &quot;ckp&quot; 문자열을 조합하여 self.save_path에 새 경로를 할당한다. 이렇게 생성된 self.save_path는 <strong>모델의 체크포인트 파일을 저장할 디렉토리 경로를 나타낸다.</strong></p>
<blockquote>
<p>self.training_name = str(int(time.time()))
        self.session = tf.Session()</p>
</blockquote>
<p>이 부분은 self.training_name이라는 클래스 멤버 변수를 초기화한다.</p>
<p>time.time() 함수는 현재 시간을 초 단위로 반환한다. 이 값은 컴퓨터 시스템의 현재 시각에 대한 타임스탬프이다.
int(time.time())는 현재 시각의 타임스탬프를 정수형으로 변환한다.
str(int(time.time()))는 이 정수 타임스탬프를 문자열로 변환한다.</p>
<p>따라서 self.training_name은 현재 시간을 문자열 형태로 나타낸 것으로, <strong>학습 중인 모델을 고유하게 식별하기 위한 이름 또는 식별자로 사용될 수 있다.</strong></p>
<p>self.session = tf.Session()은 TensorFlow에서 세션 객체(tf.Session())를 생성하여 self.session에 할당한다.
<a href="https://chan-lab.tistory.com/6">텐서플로우 세션에 대하여</a>
TensorFlow의 세션은 그래프를 실행하고 변수를 초기화하거나 학습을 진행하는 데 사용된다.
self.session은 모델을 학습하고 추론하기 위한 TensorFlow 세션을 나타내며, 이 세션을 사용하여 모델의 그래프를 실행하고 데이터를 처리할 수 있다.</p>
<p>요약하면, 이 부분은 현재 시간을 사용하여 학습 중인 모델을 식별하는 이름(self.training_name)을 생성하고, TensorFlow 세션 객체(self.session)를 초기화하는 역할을 한다. 이것들은 모델 식별 및 TensorFlow를 사용한 학습 및 추론에 필요한 초기 설정이다.</p>
<pre><code class="language-python">with self.session.as_default():
            (
                self.inputs,
                self.targets,
                self.seq_len,
                self.logits,
                self.decoded,
                self.optimizer,
                self.acc,
                self.cost,
                self.max_char_count,
                self.init,
            ) = self.crnn(max_image_width)
            self.init.run()</code></pre>
<blockquote>
<p>with self.session.as_default()</p>
</blockquote>
<p>as_default()에 대해서:
<img src="https://velog.velcdn.com/images/dong-dong_e4/post/ee99ae08-0c9a-45b7-8d7b-d22f50d182e7/image.png" alt="">
요약하자면 as_default는 텐서플로우에서 쓰는 세션 관련 함수다. 현재 세션을 기본 세션으로 설정한다.
따라서 위의 코드는 self.session을 기본 세션으로 지정한다.</p>
<p>with 문에 대해서:
<img src="https://velog.velcdn.com/images/dong-dong_e4/post/cc0e7696-d48a-4433-8784-0d98a6cc468b/image.png" alt="">
위의 코드에서는 self.crnn(max_image_width)에서 반환된 값들을 각각의 클래스 멤버 변수에 할당한다.</p>
<blockquote>
<p>self.crnn(max_image_width)</p>
</blockquote>
<p>self.crnn() 함수를 호출하여 CRNN 모델을 구성하고 초기화하는 작업을 수행한다.
max_image_width 매개변수는 모델에서 처리할 이미지의 최대 너비를 지정한다.</p>
<p>crnn 함수를 구현한 코드는 뒤에 가서 다룰 예정이니 이해가 안되더라도 일단 넘어가자.</p>
<p>self.crnn(max_image_width) 함수가 반환한 모델의 입력 데이터, 타겟 데이터, 출력(logits), 디코딩된 결과(decoded), 최적화 방법(optimizer), 정확도(acc), 손실(cost), 최대 문자 수(max_char_count), 그리고 초기화 연산(init)이 클래스 멤버 변수에 저장된다.</p>
<blockquote>
<p>self.init.run()</p>
</blockquote>
<p>TensorFlow의 모델을 사용하기 전에 필요한 초기화 연산을 실행하는 부분이다.</p>
<pre><code class="language-python">with self.session.as_default():
            self.saver = tf.train.Saver(tf.global_variables(), max_to_keep=10)
            # Loading last save if needed
            if self.restore:
                print(&quot;Restoring&quot;)
                ckpt = tf.train.latest_checkpoint(self.model_path)
                if ckpt:
                    print(&quot;Checkpoint is valid&quot;)
                    self.step = int(ckpt.split(&quot;-&quot;)[1])
                    self.saver.restore(self.session, ckpt)</code></pre>
<blockquote>
<p>self.saver = tf.train.Saver(tf.global_variables(), max_to_keep=10)   </p>
</blockquote>
<p>self.saver: Tensorflow의 saver 클래스의 인스턴스
tf.train.Saver: 모델의 상태를 저장하는 변수를 저장, 복원하는 데 사용됨
tf.global_variables(): 훈련 및 추론 과정 중 저장하고 복원해야 하는 <strong>전역 변수</strong>들
max_to_keep: 기억해야 할 checkpoints 수</p>
<blockquote>
<p>if self.restore:</p>
</blockquote>
<p>self.restore 변수가 True 이면</p>
<blockquote>
<p>print(&quot;Restoring&quot;)
                ckpt = tf.train.latest_checkpoint(self.model_path)</p>
</blockquote>
<p>이 코드는 지정된 디렉토리인 self.model_path에서 가장 최근의 체크포인트 파일을 찾아 ckpt 변수에 저장한다(체크포인트 파일은 모델의 변수 상태를 저장한 파일이다).</p>
<blockquote>
<p>if ckpt:</p>
</blockquote>
<p>체크포인트 파일(ckpt)이 존재하는 경우(체크포인트 파일이 존재하지 않을 수 있으므로 if문으로 체크해준다).</p>
<blockquote>
<p>print(&quot;Checkpoint is valid&quot;)
  self.step = int(ckpt.split(&quot;-&quot;)[1])
  self.saver.restore(self.session, ckpt)</p>
</blockquote>
<p>  checkpoint가 존재한다고 출력해 알리고, self.step에 체크포인트에서 추출한 step 번호를 저장한다. 
  self.saver.restore(self.session, ckpt): TensorFlow Saver 객체를 사용하여 모델의 변수를 지정된 체크포인트 파일(ckpt)에서 복원한다. 모델은 이전 훈련 상태로 돌아가며, 이전에 저장된 변수 값으로 초기화된다.</p>
<blockquote>
<p>self.step = int(ckpt.split(&quot;-&quot;)[1])</p>
</blockquote>
<p>개인적으로 이 부분 코드가 이해가 안 돼서 조금 더 알아봤다.</p>
<p>ckpt에 들어 있는 값은 체크포인트 파일의 경로이다(ckpt가 반환값을 받는 함수인 tf.train.latest_checkpoint(self.model_path)은 model_path에서 가장 최근의 체크포인트 파일을 찾아 그 &quot;<strong>경로</strong>&quot;를 반환환다).
체크포인트 파일은 일반적으로 하이픈과 숫자로 이루어져 있다. <code>skpt.split(&quot;-&quot;)</code>는 파일 경로에서 하이픈을 제거하고 숫자만을 남긴다.</p>
<p><code>[1]</code>은 남은 숫자 목록(배열?)에서 두 번째 요소를 의미하고 이 int값을 우리는 step으로 쓰기로 했다.</p>
<blockquote>
<p>self.data_manager = DataManager(
...
)</p>
</blockquote>
<p>DataManager 클래스는 우리가 뒤에서 뜯어볼 data_manager.py에서 정의된 클래스이다. 우리는 코드 첫부분에서 <code>from data_manager import DataManager</code>를 해 줬었다.</p>
<p>지금까지 crnn모델의 init 함수의 코드를 분석해 보았다. 이제 다음 장에서는 본격적으로 crnn 함수의 코드를 뜯어보도록 하겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[django] mysql 사용하기]]></title>
            <link>https://velog.io/@dong-dong_e4/django-mysql-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dong-dong_e4/django-mysql-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 08 Nov 2023 19:05:44 GMT</pubDate>
            <description><![CDATA[<p>django를 배우면서 많이 들리는 db라는 단어, 3학년 2학기 수업을 들으면서 데이터베이스의 기초를 배우다 보니 django가 db와 어떻게 소통하는지 정확한 과정을 알고 싶어졌다. 내가 지금 데이터베이스 과목에서 배우는 sql문과 django가 연관이 있을까? 결론부터 말하자면, 아주 깊은 연관이 있었다.</p>
<p>Django에는 ORM이라는 기능이 있다. Object Relational Mapping의 약자로, 말 그대로 object(객체)를 relational mapping(관계형 데이터베이스와 매핑)해준다는 것이다. 즉, ORM을 사용하면 sql문을 사용하지 않고 python만으로도 db에 접근할 수 있다.</p>
<p>예를 들어</p>
<pre><code class="language-python">from django.db import models

class User(models.Model):
    name     = models.CharField(max_length=45)
    email    = models.CharField(max_length=100, unique=True)
    password = models.CharField(max_length=200)

    class Meta:
        db_table = &#39;users&#39;</code></pre>
<pre><code class="language-python">mysql&gt; desc users2;
+----------+--------------+------+-----+---------+-------+
| Field    | Type         | Null | Key | Default | Extra |
+----------+--------------+------+-----+---------+-------+
| id       | bigint       | NO   | PRI | NULL    |       |
| name     | varchar(45)  | YES  |     | NULL    |       |
| email    | varchar(100) | YES  | UNI | NULL    |       |
| password | varchar(200) | YES  |     | NULL    |       |
+----------+--------------+------+-----+---------+-------+
4 rows in set (0.00 sec)</code></pre>
<p>이렇게 테이블을 추가해 줄 수 있다. 원래 이 작업은 다음과 같이 sql 쿼리문을 사용하는 것이다.</p>
<pre><code class="language-mysql">CREATE TABLE users2 (
    id BIGINT NOT NULL PRIMARY KEY,
    name VARCHAR(45),
    email VARCHAR(100) UNIQUE,
    password VARCHAR(200)
    );</code></pre>
<p>django의 ORM기능은 sql쿼리문을 몰라도 아주 쉽게 db에 접근할 수 있도록 해 주는 아주 유용한 기능이다. 하지만 정말 쿼리문을 몰라도 개발에 아무 지장이 없을까? 그렇지 않다.</p>
<p><a href="https://daeguowl.tistory.com/171">링크텍스트</a>
이 블로그 글에서는 쿼리문에 대한 이해 없이 ORM만으로만 개발하다 어려움을 겪은 신입 개발자의 이야기가 담겨 있다. 아무래도 ORM만을 사용하면 실제로 쿼리가 어떻게 동작하는지 이해할 기회가 없기도 하고, 실제 쿼리문을 사용하여 해결해야 할 문제가 생겼을 때 제대로 대처하지 못하게 될 수 있다. db에서 복잡한 로직을 수행할 때 ORM만으로는 절대 원하는 성능을 구현해 낼 수 없을 때가 있기 때문이다. 위 글에서도 나와있듯 ORM을 사용하면 당연히 쿼리문을 직접 사용하는 것보다는 성능이 떨어질 수밖에 없고, 실무에서는 이 문제가 치명적으로 작용할 수도 있다.</p>
<p>ORM이 편하고 좋은 기능이고, 막 django를 배우기 시작한 입장에서는 ORM에 의존할 수밖에 없지만, 절대 실제 쿼리문에 대한 이해를 게을리해서는 안 되는 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Javascript] 함수 선언문 vs 함수 표현식]]></title>
            <link>https://velog.io/@dong-dong_e4/Javascript-%ED%95%A8%EC%88%98-%EC%84%A0%EC%96%B8%EB%AC%B8-vs-%ED%95%A8%EC%88%98-%ED%91%9C%ED%98%84%EC%8B%9D</link>
            <guid>https://velog.io/@dong-dong_e4/Javascript-%ED%95%A8%EC%88%98-%EC%84%A0%EC%96%B8%EB%AC%B8-vs-%ED%95%A8%EC%88%98-%ED%91%9C%ED%98%84%EC%8B%9D</guid>
            <pubDate>Wed, 02 Aug 2023 05:16:54 GMT</pubDate>
            <description><![CDATA[<h3 id="1-함수-선언문">1. 함수 선언문</h3>
<pre><code>function HW(){
  console.log(&quot;Hello World&quot;);
}
HW();</code></pre><h3 id="2-함수-표현식">2. 함수 표현식</h3>
<pre><code>let HW function(){
  console.log(&quot;Hello World&quot;);
}
HW();</code></pre><h3 id="3-차이점">3. 차이점</h3>
<p>둘의 차이가 무엇일까? 둘의 차이를 이해하기 위해서는 javascript의 언어적 특성을 이해할 필요가 있다.</p>
<p>javascript는 인터프리터 언어이다. 인터프리터 언어는 순차적으로 실행되고 즉시 결과를 반환하는 언어이다. 따라서 다음과 같은 코드는 실행되지 않는다.
<img src="https://velog.velcdn.com/images/dong-dong_e4/post/2b8eeb65-f69b-476c-b098-f6f92a065839/image.png" alt="">
console.log문 전에 변수를 선언하지 않고 사용해 버렸기 때문이다. 코드를 순차적으로 읽어나가는 인터프리터 언어의 특성 때문에 벌어지는 일이다. 그렇다면 다음의 코드에서도 같은 일이 벌어지리라고 예상할 수 있다.</p>
<pre><code>HW();
function HW(){
  console.log(&quot;Hello World&quot;);
}</code></pre><p>하지만 javascript에서는 가능한 문법이다. 다음과 같이 함수가 정상적으로 호출되어 값이 출력되는 것을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/dong-dong_e4/post/a6c8400b-b8d4-4981-89a6-d2ffa5755f9b/image.png" alt=""></p>
<p>변수를 사용할 때는 가능하지 않았던 일이 함수를 호출할 때는 어떻게 가능한 걸까? 이는 javascript 만의 독특한 알고리즘 때문이다.</p>
<blockquote>
<p>hoisting 기능</p>
</blockquote>
<p>javascript는 실행 전 초기화 단계에서 코드의 모든 함수를 찾아 생성해 둔다. 이를 hoisting이라고 한다.
겉보기에는 함수 호출을 선언문 다음에 할 수 있을 것 같지만, 실제로 호출 가능 범위는 위로 끌어 올려진다.</p>
<h4 id="단-코드-자체가-위로-재배치되는-것은-아니다">단 코드 자체가 위로 재배치되는 것은 아니다!</h4>
<blockquote>
<p>따라서 함수 선언문은 비교적 위치에 상관없이 자유롭게 사용할 수 있고 에러의 가능성도 작다. 따라서 함수 표현식보다는 함수 선언문을 활용하는 것이 더 편하고 안전하다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[JavaScript 사용자에게서 값 입력받기]]></title>
            <link>https://velog.io/@dong-dong_e4/JavaScript-%EC%82%AC%EC%9A%A9%EC%9E%90%EC%97%90%EA%B2%8C%EC%84%9C-%EA%B0%92-%EC%9E%85%EB%A0%A5%EB%B0%9B%EA%B8%B0</link>
            <guid>https://velog.io/@dong-dong_e4/JavaScript-%EC%82%AC%EC%9A%A9%EC%9E%90%EC%97%90%EA%B2%8C%EC%84%9C-%EA%B0%92-%EC%9E%85%EB%A0%A5%EB%B0%9B%EA%B8%B0</guid>
            <pubDate>Wed, 02 Aug 2023 04:25:46 GMT</pubDate>
            <description><![CDATA[<p>javascript에서 사용하는 입력 함수 alert, prompt, confirm 함수에 대해서 알아보자.</p>
<h3 id="1-alert-메세지를-보여준다">1. alert: 메세지를 보여준다</h3>
<p><img src="https://velog.velcdn.com/images/dong-dong_e4/post/9579595b-d6fd-4bf3-bae5-64716c222e2a/image.png" alt="">
사용자에게 이름을 입력받고 그대로 출력하는 메세지를 띄우고 싶다.
<img src="https://velog.velcdn.com/images/dong-dong_e4/post/63d60841-b189-4a7c-a650-f9368c1bfaa8/image.png" alt="">
뒤에서 다룰 prompt 함수를 통해 이름을 Mike로 입력받고 출력한다.
<img src="https://velog.velcdn.com/images/dong-dong_e4/post/01aedc95-355b-48db-9b82-8f6dbc82d228/image.png" alt="">
다음과 같이 안내창이 출력된다.
<img src="https://velog.velcdn.com/images/dong-dong_e4/post/5e8da125-67b1-49d6-8922-08372b5dc2b8/image.png" alt="">
사용자가 &#39;확인&#39;을 누르면 </p>
<h3 id="2-prompt-사용자에게-메세지를-보여주고-값을-입력받음">2. prompt: 사용자에게 메세지를 보여주고 값을 입력받음</h3>
<p><img src="https://velog.velcdn.com/images/dong-dong_e4/post/5672ee86-a85a-4de6-b3a9-5c3bf53f58ae/image.png" alt=""></p>
<p>학생 두 명의 나이를 입력받고 두 학생의 나이 평균을 계산해 주는 코드
다음의 코드를 입력하면
<img src="https://velog.velcdn.com/images/dong-dong_e4/post/7b42b399-d56c-4281-832b-bf0b0a619eae/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dong-dong_e4/post/e532b730-4ebb-46d1-ba50-f01b9cb8e83c/image.png" alt=""></p>
<p>이런 식으로 age1과 age2에 사용자가 값을 입력해 줄 수 있다.
각각 22, 24 라는 값을 넣고 실행해 주겠다.</p>
<p>그러나 결과를 실행해 보면 예상된 23의 값이 아니라
<img src="https://velog.velcdn.com/images/dong-dong_e4/post/c7b7bfb6-fa77-45b8-9ba7-1a7c230cde7a/image.png" alt="">
다음과 같이 전혀 엉뚱한 값이 나오는데 그 이유는 prompt 함수는 사용자로부터 &quot;문자열&quot;로 값을 입력받기 때문이다.</p>
<p>따라서 (22+24)/2 = 46/2 = 23 이 아니라 (&quot;22&quot;+&quot;24&quot;) = &quot;2224&quot;/2 = 1112로 출력되는 것</p>
<p>여기서 &quot;2224&quot;/2의 계산이 가능한 이유는 자동 형변환 때문이다. </p>
<p>여기서 자동 형변환의 맹점이 잘 드러난다. 자동 형변환은 사용자가 일일이 형변환을 하지 않아도 컴파일러가 자동으로 형변환해 계산을 해 주는 편리한 기능이기도 하지만 원인을 찾기 힘든 에러의 원인이 되기도 한다.</p>
<p>그렇기에 명시적 형변환을 꼭 해 주어야 한다.
prompt 함수를 통해 받은 문자열 값을 Number() 함수를 통해 형변환해 주겠다.
<img src="https://velog.velcdn.com/images/dong-dong_e4/post/7c2f40d4-b388-4eee-ae09-328c463d84df/image.png" alt="">
Number() 함수는 string 값을 숫자형으로 변환해 준다.
<img src="https://velog.velcdn.com/images/dong-dong_e4/post/dc0ae40d-7fd0-4001-bd67-813c8c0ccdd2/image.png" alt="">
예상된 값이었던 23이 올바르게 나오는 것을 볼 수 있다.</p>
<h3 id="3-confirm-사용자에게-확인취소-값-받음">3. confirm: 사용자에게 확인/취소 값 받음</h3>
<p><img src="https://velog.velcdn.com/images/dong-dong_e4/post/e30a12cf-b923-487d-9c5f-21aa56c10175/image.png" alt="">
사용자가 성인인지를 묻는 안내창을 띄우려고 한다. 사용자는 확인/취소로 대답한다.
<img src="https://velog.velcdn.com/images/dong-dong_e4/post/dbd60e1e-dcf2-4cd5-8aa5-ec18baf448e2/image.png" alt=""></p>
]]></description>
        </item>
    </channel>
</rss>