<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>ChanKim</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Mon, 08 Dec 2025 23:55:03 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>ChanKim</title>
            <url>https://velog.velcdn.com/images/chan_woo_00/profile/e719fa9b-2a98-4d6d-a0b3-0e92a20e90aa/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. ChanKim. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/chan_woo_00" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[C++ 재활용 문제 풀이]]></title>
            <link>https://velog.io/@chan_woo_00/C-%EC%9E%AC%ED%99%9C</link>
            <guid>https://velog.io/@chan_woo_00/C-%EC%9E%AC%ED%99%9C</guid>
            <pubDate>Mon, 08 Dec 2025 23:55:03 GMT</pubDate>
            <description><![CDATA[<h4 id="자릿수-더하기">자릿수 더하기</h4>
<p>숫자의 각 자리수를 더하여 반환하는 문제입니다.</p>
<pre><code class="language-C++">#include &lt;iostream&gt;

using namespace std;
int solution(int n)
{
    int answer = 0;

    while(n &gt; 0){
        answer += (n % 10);
        n /= 10;
    }

    cout &lt;&lt; &quot;Hello Cpp&quot; &lt;&lt; endl;

    return answer;
}</code></pre>
<p>다른 사람 풀이로는 to_string 사용하여 각 위치의 값을 더하는 방식을 사용하였는데 이 과정에서s[i] - &#39;0&#39;으로 아스키 코드 값을 제거해서 숫자 계산하였습니다.</p>
<pre><code class="language-C++">#include &lt;iostream&gt;
#include &lt;string&gt;

using namespace std;
int solution(int n)
{
    int answer = 0;

    string s = to_string(n);

    for(int i = 0; i &lt; s.size(); i++) answer += (s[i] - &#39;0&#39;);

    // [실행] 버튼을 누르면 출력 값을 볼 수 있습니다.
    cout &lt;&lt; &quot;Hello Cpp&quot; &lt;&lt; endl;

    return answer;
}</code></pre>
<h4 id="올바른-괄호">올바른 괄호</h4>
<p>괄호의 특성을 확인하여 열린 괄호와 닫힌 괄호가 정상적으로 이루어졌는지 파악하는 코드입니다.</p>
<pre><code class="language-C++">#include&lt;string&gt;
#include &lt;iostream&gt;

using namespace std;

bool solution(string s)
{
    bool answer = true;
    int left_open = 0;
    int right_close = 0;
    for(int i = 0; i &lt; s.size(); i++){
        if(s.at(i) == &#39;(&#39;) left_open++;
        else if(s.at(i) == &#39;)&#39;) right_close++;
        if (right_close &gt; left_open) {answer = false; break;}
    }
    if(right_close != left_open) answer = false;

    cout &lt;&lt; &quot;Hello Cpp&quot; &lt;&lt; endl;

    return answer;
}</code></pre>
<p>각 괄호의 수에 대한 변수를 계산하여 True, False를 확인했지만 하나의 변수에 대한 증감식을 사용하여 계산하는것이 더욱 깔끔하다고 생각됩니다.(단 &#39;)&#39;가 먼저 들어오는 경우에 대한 예외조건이 필요합니다.)</p>
<h4 id="짝-지어-제거하기">짝 지어 제거하기</h4>
<p>연속된 문자가 나오는 경우에 제거하여 문자열이 남아있는 경우 0 문자열이 모두 사라진 경우 1을 반환하는 코드입니다.</p>
<pre><code class="language-C++">#include &lt;iostream&gt;
#include &lt;string&gt;
using namespace std;

int solution(string s)
{
    int answer = -1;

    for(int j = 0; j &lt; s.size(); ){
        if(s.at(j) == s[j+1]) {
            s.erase(j, 2); 
            j = j-2;
            if(j = s.size()) j = 0;
        }
        else j++;
    }
    if(s.size() == 0) answer = 1;
    else answer = 0;

    cout &lt;&lt; s &lt;&lt; endl;

    return answer;
}</code></pre>
<p>초기 풀이 코드로 string의 위치를 찾아가 현재 위치와 다음 위치가 같다면 삭제를 진행하는 방식을 사용하였습니다 j+1위치의 경우 string객체의 범위를 벗어나게 되어 at() 함수 대신 배열 접근 방법을 사용하였습니다.</p>
<pre><code class="language-C++">#include &lt;iostream&gt;
#include &lt;stack&gt;
#include &lt;string&gt;

using namespace std;

int solution(string s)
{
    int answer = -1;

    stack&lt;char&gt; cw;
    char tmp;
    for(int j = 0; j &lt; s.size(); j++){
        if(!cw.empty()) tmp = cw.top();
        cw.push(s.at(j));
        if(tmp == cw.top()){
            cw.pop();
            cw.pop();
            tmp = &#39; &#39;;
        }
    }
    if (cw.empty()) answer = 1;
    else answer = 0;

    return answer;
}</code></pre>
<p>다른 사람들의 풀이를 보고 stack을 사용한 방식입니다.
인덱스로 접근하는 방식이 아니라 인덱스 관련 문제가 존재하지 않습니다.</p>
<h4 id="피보나치-수열">피보나치 수열</h4>
<p>피보나치 수열 문제입니다.
마지막 값에 대해서 1234567로 나머지를 구한 값을 반환합니다.</p>
<pre><code class="language-C++">#include &lt;string&gt;
#include &lt;vector&gt;

using namespace std;
int fibonacci(int n);

int fib[100002];

int solution(int n) {
    int answer = 0;

    answer = fibonacci(n);

    return answer;
}

int fibonacci(int n){
    if(n&lt;=1) {
        fib[n] = n;
        return n;
    }
    else if(fib[n] != 0) return fib[n];
    else {
        fib[n] = (fibonacci(n-1) + fibonacci(n-2)) % 1234567;
        return fib[n];
    }
}</code></pre>
<p>n은 2 이상 100,000 이하인 자연수로 해당 방식에서 마지막 answer에 값을 할당해주는 과정에서 1234567의 값에 대한 나머지를 구하게 되는 경우 int의 범위를 벗어나게 되어 피보나치 함수 내부에서 나눠줌으로서 해당 문제를 해결하였습니다.</p>
<h4 id="숫자의-표현">숫자의 표현</h4>
<p>연속된 자연수의 합으로 입력된 값이 나오는 경우의 수를 출력하는 문제입니다.</p>
<pre><code class="language-C++">#include &lt;string&gt;
#include &lt;vector&gt;

using namespace std;

int solution(int n) {
    int answer = 0;

    int start_num = 1;

    while(true){
        int sum = 0;
        int first_num = start_num;
        if(start_num == n || start_num &gt; (n / 2 + 1)){
            answer++;
            break;
        }
        while(sum &lt; n){
            sum += first_num++;
            if(sum == n) answer++;
        } start_num++;

    }
    return answer;
}</code></pre>
<p>숫자가 시작되는 값에 대해 반복문을 수행하여 경우의 수를 반환하였습니다.</p>
<h4 id="jadencase-문자열-만들기">JadenCase 문자열 만들기</h4>
<pre><code class="language-C++">#include &lt;string&gt;
#include &lt;vector&gt;

using namespace std;

string solution(string s) {
    string answer = &quot;&quot;;

    for(int i = 0; i &lt; s.length(); i++){
        if(s[i - 1] == &#39; &#39; &amp;&amp; s.at(i) &gt;= &#39;a&#39; &amp;&amp; s.at(i) &lt;= &#39;z&#39;) {
            answer += s.at(i) - (&#39;a&#39;- &#39;A&#39;);
        }
        else if(s[i - 1] != &#39; &#39; &amp;&amp; s.at(i) &gt;= &#39;A&#39; &amp;&amp; s.at(i) &lt;= &#39;Z&#39;) {
            answer += s.at(i) + (&#39;a&#39;- &#39;A&#39;);
        }
        else if(s.at(0) &gt;= &#39;0&#39; || s.at(0) &lt;= &#39;9&#39;) {answer += s.at(i);}
    }
    if(answer.at(0) &gt;= &#39;a&#39; &amp;&amp; answer.at(0) &lt;= &#39;z&#39;) answer.at(0) -= (&#39;a&#39;- &#39;A&#39;);

    return answer;
}</code></pre>
<p>조건문 사용하여 각 조건에 대한 문제 풀이를 진행하였습니다.
아스키 코드로 각 문자에 대한 변환 실행했습니다.
이후 유저들 풀이에서 toupper와 tolower함수 사용하여 풀이한 것을 확인하였습니다.</p>
<h4 id="연속-부분-수열-합의-개수">연속 부분 수열 합의 개수</h4>
<pre><code class="language-C++">#include &lt;string&gt;
#include &lt;vector&gt;
#include &lt;set&gt;

using namespace std;

void get_sum(set&lt;int&gt; &amp;sum, const vector&lt;int&gt;&amp; vec, int len);

int solution(vector&lt;int&gt; elements) {
    int answer = 0;
    set&lt;int&gt; sum;

    for(int len = 1; len &lt;= elements.size(); len++){
        get_sum(sum, elements, len);
    }

    answer = sum.size();
    return answer;
}

void get_sum(set&lt;int&gt; &amp;sum, const vector&lt;int&gt;&amp; vec, int len){
    int size = vec.size();

     for(int i = 0; i &lt; size; i++){
        int tmp = 0;

         for(int j = 0; j &lt; len; j++){
            int idx = (i + j) % size;
            tmp += vec[idx];
        }
        sum.insert(tmp);
    }
}</code></pre>
<p>set : sum을 외부 함수로 넘겨 이중 for 문으로 각 벡터에 대해 연산하고 set에 추가하는 방식을 사용하였습니다.
하지만 for 내부에서 이중 for문인 get_sum을 불러와 시간에 대한 복잡도가 높습니다.</p>
<pre><code class="language-C++">#include &lt;string&gt;
#include &lt;vector&gt;
#include &lt;set&gt;

using namespace std;

int solution(vector&lt;int&gt; elements) {
    set&lt;int&gt; S;

    int n = elements.size();

    for (int i = 0 ; i &lt; n ; ++i) {
        int sum = 0;
        for (int j = i ; j &lt; i + n ; ++j) {
            sum += elements[j % n];
            S.insert(sum);
        }
    }

    return S.size();
}</code></pre>
<p>다른 풀이로는 이중 for문으로 계산한 방식으로 이중 for문을 하나의 for문으로 작성하여 시간 복잡도를 줄였습니다.</p>
<h4 id="점프와-순간이동">점프와 순간이동</h4>
<p>K칸 이동은 에너지 1 사용, 현재 이동한 위치 x 2의 경우에는 에너지를 소비하지 않을 경우 최소한의 에너지를 사용하여 목적지 n까지 이동하는 문제입니다.</p>
<pre><code class="language-C++">#include &lt;iostream&gt;
using namespace std;

int solution(int n)
{
    int ans = 0;

    while(n != 1){
        if(n % 2 == 1) {n -= 1; ans++;}
        else n /=2;
    } ans++;

    cout &lt;&lt; &quot;Hello Cpp&quot; &lt;&lt; endl;

    return ans;
}</code></pre>
<p>시작에서 K칸 이동과 이동하는 위치를 계산하는 방법도 있지만 뒤에서 시작하면 더 간편히 써지지 않을까 하여 뒤에서부터 시작하였습니다.</p>
<pre><code class="language-C++">#include &lt;iostream&gt;
using namespace std;

int solution(int n)
{
    int ans = 1;
    int len = 1;
    while(len != n){
        if(len * 2 &lt;= n) len *= 2;
        else {len++; ans++;}
    } 

    cout &lt;&lt; &quot;Hello Cpp&quot; &lt;&lt; endl;

    return ans;
}</code></pre>
<p>다음은 시작 부분에서 진행한 코드입니다. 다음 코드에서의 문제점은 ans 증가 이후 2배를 하는 것이 더 효율적인지 2배를 한 이후 ans를 증가시키는 것이 더 효율적인지에 대한 보장이 없다는 것입니다. 따라서 해당 코드의 경우에는 BFS 알고리즘을 사용해야 합니다.</p>
<pre><code class="language-C++">#include &lt;iostream&gt;
#include &lt;vector&gt;
#include &lt;queue&gt;

using namespace std;

int solution(int n)
{
    // 현재 위치, 현재까지 쓴 배터리
    queue&lt;pair&lt;int, int&gt;&gt; q;

    q.push({0, 0});

    vector&lt;int&gt; visited(n + 1, 2100000000);
    visited[0] = 0;

    while(!q.empty()){
        int curr = q.front().first;
        int cost = q.front().second;
        q.pop();

        // 적은 배터리로 이곳에 온 적이 있다면, 지금 경로는 폐기
        if(visited[curr] &lt; cost) continue;

        // 순간이동 (*2)
        if(curr * 2 &lt;= n &amp;&amp; visited[curr * 2] &gt; cost) {
            visited[curr * 2] = cost;
            q.push({curr * 2, cost});
        }

        // 점프 (+1)
        if(curr + 1 &lt;= n &amp;&amp; visited[curr + 1] &gt; cost + 1) {
            visited[curr + 1] = cost + 1;
            q.push({curr + 1, cost + 1});
        }
    }

    return visited[n];
}</code></pre>
<p>DP도 생각해보았으나 DP는 이전 진행된 값이 고정되어야 하기에 쓰지 못하였습니다.</p>
<h4 id="n개의-최소공배수">N개의 최소공배수</h4>
<p>하나의 벡터에 있는 모든 원소들에 대한 최소 공배수 값을 구하는 문제입니다.</p>
<pre><code class="language-C++">#include &lt;string&gt;
#include &lt;vector&gt;
#include &lt;algorithm&gt;

using namespace std;

int gcd(int a, int b);
int lcm(int a, int b);

int solution(vector&lt;int&gt; arr) {
    int answer = 0;

    for (int i = 0; i &lt; arr.size() - 1; i++){
        arr.at(i + 1) = lcm(arr.at(i), arr.at(i + 1));
    } answer = arr.at(arr.size() -1);

    return answer;
}

int gcd(int a, int b)
{
    int c;
    while (b != 0)
    {
        c = a % b;
        a = b;
        b = c;
    }
    return a;
}
int lcm(int a, int b)
{
    return a * b / gcd(a, b);
}</code></pre>
<p>최대공배수를 구하기 위한 최소공배수 함수 gcd를 생성하였습니다.
이전의 값의 최대 공배수를 새로운 비교값 원소로 설정하였습니다.
최소공배수를 구하는 과정에서 원소들간 작은 수가 높은 수의 약수인 경우를 제거하고 최대공배수를 구하면 효율성이 올라갈 수 있을까 생각해보았지만 제거하는 과정에서의 for문에 들어가는 비용이 더 높아 도중 제외했습니다.</p>
<h4 id="영어-끝말잇기">영어 끝말잇기</h4>
<p>주어진 vector<string>객체를 기반으로 나열이 되었을 끝말잊기를 틀린 사람이 있는지 파악하고 몇번째 어느 사람이 틀렸는지 이야기하는 문제입니다.</p>
<pre><code class="language-C++">#include &lt;string&gt;
#include &lt;vector&gt;
#include &lt;iostream&gt;
#include &lt;algorithm&gt;

using namespace std;

vector&lt;int&gt; solution(int n, vector&lt;string&gt; words) {
    vector&lt;int&gt; answer;

    for(int i = 1; i &lt; words.size(); i++) {
        if(words[i-1].back() != words[i].front() || 
           find(words.begin(), words.begin() + i, words[i]) != words.begin() + i) {
            answer.push_back((i % n) + 1);
            answer.push_back((i / n) + 1);
            return answer;
        }
    }

    answer.push_back(0);
    answer.push_back(0);

    return answer;
}</code></pre>
<p>if문은 마지막 글자와 첫번째 글자의 맞고 틀림을 검사하는것과 동시에 find 함수로 찾은 이터레이터값이 마지막 위치와 같은지를 확인하여 틀리다면 이전 앞에서 먼저 사용한 것으로 판단하여 해당 위치에서의 반복 횟수와 사람의 인덱스를 answer에 추가하는 방식입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Multi-Task model 이해]]></title>
            <link>https://velog.io/@chan_woo_00/Multi-Task-model</link>
            <guid>https://velog.io/@chan_woo_00/Multi-Task-model</guid>
            <pubDate>Fri, 24 Oct 2025 02:03:51 GMT</pubDate>
            <description><![CDATA[<p>You Only Look at Once for Real-Time and Generic Multi-Task</p>
<p>논문을 기반으로 작성하였고 서베이 논문을 포함하여 Multi-Task model 자체에 대한 설명을 하는 글.</p>
<p>이전까지의 딥러닝 모델들은 하나의 입력에 대해 하나의 출력값을 가진다.</p>
<p>Image Classification : 객체의 클래스</p>
<p>Object Detection : 객체의 클래스와 BBox값.</p>
<p>Image Segmentation : 객체의 클래스와 객체의 마스크.</p>
<p>객체의 클래스가 Object Detection과 Image Segmentation에 모두 포함되는 이유는 Head의 구조에 있다.</p>
<p>YOLOv8의 Head는 Decoupled Head를 사용하는데 Classification Branch와 Regression Branch로 구성되어 Classification Branch 에서는 클래스 확률을 예측하고 Regression Branch 에서는 BBox 좌표와 Objectness Score를 예측한다.</p>
<p>각 Head에 대해서 손실 함수의 종류를 선택하고 loss값의 가중치를 조절하는 등의 작업을 통해 균형을 이룬다.</p>
<p>-&gt; YOLOv10의 경우에서는 Classification Branch가 Regression Branch보다 모델의 성능에 덜 영향을 미치는 것을 확인하여 해당 
Head에 들어가는 파라미터의 값을 줄이는 작업도 진행한다.</p>
<img src=https://velog.velcdn.com/images/chan_woo_00/post/5ecf32d8-8203-486f-a1f0-6382a806c0a2/image.png>

<p>위 단락까지만 보게 된다면 Object detection과 Image Segmentation작업도 하나의 Multi-Task라고 볼 수 있다.(Head라 통칭한 구조 내부에서 Classification과 Detection or Segmentaion작업을 수행하여 출력값이 2개 이상이므로.) 하지만 Detection 작업을 수행하려면 객체가 무었인지부터 파악해야 하는데 이 과정이 Classification 작업이므로 Object Detect과 Image Segmentation작업을 수행할 때 Classification Branch가 존재하는 것에 대하여 Multi-Task Model이라고 엄격히 표시하지는 않는 듯 하다.</p>
<p>(Detection과 Segmentation은 BBox와 Mask 계산에 사용되는 값들이 달라 Multi-task라고 확실히 불린다.)</p>
<img src=https://velog.velcdn.com/images/chan_woo_00/post/f44b7357-8fe3-4f19-85b4-5399ff699dff/image.png>
물론 YOLOR의 논문처럼 multi-task라고 표시해주는 경우도 존재한다.



<p>다시 되돌아와 Multi-Task의 본질은 하나의 Input값에 대하여 2개 이상의 output을 출력하는 작업들을 의미한다.</p>
<p>YOLO series의 경우에는 Input이 이미지이고 output이 classfication, detection, segmentation이 된다.</p>
<p>이미지에 대한 Multi-Task를 목적으로 하는 작업들은 대부분 detection과 segmentation 작업을 합치는 것으로 자율주행에서는 차량과 사람 등에 장애물에 대한 BBox를 구하고 Line Lane과 Drivable Area에 대한 Mask들을 추출한다.</p>
<p>결국 중요한 요점은 모델의 구조를 설계할 때 어디까지 공유를 하고 하이퍼 파라미터들을 어떻게 설정하는지가 된다.</p>
<p>A-YOLOM의 경우 Backbone을 공유하고 Neck과 Head를 모두 분리시키는 방법을 사용한다.
<img src=https://velog.velcdn.com/images/chan_woo_00/post/b83fb646-2f8d-4191-8c99-84777252fcc8/image.png>
해당 논문에서는 Backbone을 공유하여 특성 추출단계에서의 연산량을 줄이고 AC 블록을 통해 Neck에서의 연산량을 추가적으로 줄임과 동시에 불필요한 특성들을 가져오지 않도록 하였다.</p>
<p>DRMNet의 경우에는 Backbone과 Neck을 공유하고 Head를 분리시켜 사용한다.
<img src=https://velog.velcdn.com/images/chan_woo_00/post/6ec7eeb6-7eed-4c84-bbc0-dc9c29045ccf/image.png></p>
<p>Neck을 공유하지 않는 듯 하지만 Detail Branch(Conv) 이후 Segmentic Branch에서 생성된 Neck의 특성맵을 받은 이후에 Head로 분리되는 것을 확인할 수 있다.</p>
<p>DLT-Net의 경우에는 Traffic Object Decoder라는 추가적인 객체를 검출한다
<img src="https://velog.velcdn.com/images/chan_woo_00/post/505a138b-62f4-446a-ba78-6d138fa8d690/image.png" alt="">
당 논문은 Drivable Area 내부에 Lane Line이 존재하고 Drivable Area로 취급되지 않는 영역에 대해서 Traffic Object가 있는 것에 집중하여 구조를 설계하였다&lt;마지막 특성맵이 차원 수가 256이었으나 128로 줄어든 것은 point-wise conv작업을 진행한 것으로 보인다.(github에서 확인해보려 하였으나 존재하지 않아 확인은 실패.) &gt;. Backbone과 Neck을 공유하고 Context Tensor영역을 추가하여 Drivable Area 내부에 있는 특성맵을 공유하는 형식으로 진행된다. Context Tensor를 통과할 때 Traffic Object는 Concatenation작업을 통해 최종적으로 80<em>45</em>256의 특성맵을 가지게 되고 Lane Line Decoder의 경우에는 Element-wise Addition작업을 수행하여 Drivable Area Decoder와 같은 80<em>45</em>128의 특성맵을 가지게 된다.</p>
<p>YOLOP는 Neck구조까지 공유를 하지만 Detect Head는 모든 특성맵을 사용하는 반면 Segment Head들은 Neck의 가장 큰 사이즈의 특성맵만을 받아 사용하는 구조를 가진다.(사실 YOLOP만의 특성은 아니고 많이 사용한다.)
<img src="https://velog.velcdn.com/images/chan_woo_00/post/ce6652f9-9040-4050-9b77-316c6d31cd20/image.png" alt=""></p>
<p>위 사례들처럼 Multi-Task작업을 구현하는 방식은 여러가지이다.</p>
<ul>
<li>Backbone만을 공유하는 사례.</li>
<li>Neck까지 공유를 하지만 Segment Head에서는 Neck의 일부분만 추출하여 사용하는 사례.</li>
<li>Neck까지 모두 공유를 하고 Drivable Area의 특성을 다른 Head에 공유하는 사례.</li>
</ul>
<p>이 외에도 보지 못한 논문들과 사례들이 많겠지만 기본적으로 Backbone은 공유를 하고 Neck은 선택적으로 공유를 하거나 아에 분리시키는 것을 목표로 한다.</p>
<p>특이한 점은 A-YOLOM처럼 Neck의 단계에서 분리를 시킬 때 Segment Neck과 Detect Neck 2가지로 분리시키지 않고 Segment Neck를 2개를 사용하여 총 3개의 Neck을 사용했다는 것이다.</p>
<p>Loss를 기반으로 생각할 때 Multi-Task의 Loss값은 아래와 같이 계산된다.</p>
<p>(각 모델마다 계산하는 방식이 차이는 있지만 전체적인 프레임은 아래와 같다.)</p>
<p>( Loss_1 : Detection loss, Loss_2 : Drivable area loss, Loss_n : Lane line loss &lt;- 부여된 값 예시 )</p>
<p>[ Total Loss = W_1 * Loss_1 + W_2 * Loss_2 + ... + W_n * Loss_n ]</p>
<p>계산된 Loss값은 각 Head에 동일하게 부여되어 최적화 과정을 거치게 되는데 Neck을 공유하게 된다면 Head에서 계산되어 나온 Loss값을 합친다.</p>
<p>( w1 : Detection weight, w2 : Drivable weight, w3 : Lane weight )</p>
<p>[ Total Neck Loss = w1 * Loss_detection_head + w2 * Loss_drivable_head + w3 * Loss_lane_head ]
<img src="https://velog.velcdn.com/images/chan_woo_00/post/e14104c9-0397-4061-a425-0a00d162e1b2/image.png" alt=""></p>
<p>해당 작업에서 각 작업에 주어지는 Weight값들은 일반적으로 직접 부여하게 된다. (weight값은 영향력, 기여도 등으로 이해해도 좋다.)</p>
<p>이후 Backbone은 Neck이 하나로 이루어져 있다면 하나의 Loss값을 받게 되고 Backbone만을 공유한다면 Neck에서 진행된 과정을 Backbone에서 거치게 된다. 하지만 위에서 언급한 것처럼 Neck을 2개를 사용하고 다시 한번 Neck에서 2개의 Head를 구성하게 되는 경우에는 Loss값을 합치는 과정을 2번 격게 된다. 이 과정에서 각 작업에 대한 Weight값(Neck에서 2번 Backbone에서 2번)을 설정하는 과정도 쉽지 않으며 Head의 수가 달라 절대적인 Gradient값에 대해서도 Head가 많은 경우의 Neck이 높은 값을 가지게 되어 하나의 Head를 가진 Neck의 작업이 낮은 성능을 가지게 되는 경우가 있어 비대칭의 학습이 이루어질 수 있다.</p>
<p>다음과 같은 문제점을 소지하고 있고 해당 작업을 가중치 자동화 작업(Uncertainty-based Weighting)과 중간의 Neck전용 Loss( Auxiliary Loss)값을 추가해주는 방식 등을 통해 완화시킬 수 있지만 다른 구조를 사용하는 편이 더 간편하고 효율성이 높아 Backbone도 공유하고 Neck을 공유함과 동시에 분리시키는 구조는 거의 사용되지 않는다.</p>
<p>종합적으로 평가하였을 때 Multi-Task에서 중요하게 판단하는 부분을 나열하면 다음과 같다.</p>
<ul>
<li>Backbone까지 공유할 것인지 Neck까지 공유할 것인가.</li>
<li>Neck까지 공유하는 경우에 Segmentation 작업에 대해 어느 수준의 정보를 제공할 것인가.
ㄴ Detection작업의 경우 작은 객체에 대한 인식이 필요하여 모든 사이즈 레벨의 특성맵을 모두 사용한다.
ㄴ 위 과정에서 FPN Network뿐만 아니라 PAN Network도 사용하는 경우가 다수이다.<ul>
<li>Loss값을 계산할 때 어떠한 방식으로 손실 값을 계산할 것인가.
ㄴ Object detection의 경우 CIoU, DIoU, dfl, softmax cross, L1, L2 등
ㄴ Image Segmentation의 경우 Cross-Entropy, Focal, IoU 등</li>
<li>작업을 모두 거쳤을 때 FLOPs값의 크기</li>
<li>작업을 모두 거쳤을 때 정확도와 클래스 인식률.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[YOLOv8_Multi_task 논문 테스트(동영상) ]]></title>
            <link>https://velog.io/@chan_woo_00/YOLOv8Multitask-%EB%85%BC%EB%AC%B8-%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%8F%99%EC%98%81%EC%83%81</link>
            <guid>https://velog.io/@chan_woo_00/YOLOv8Multitask-%EB%85%BC%EB%AC%B8-%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%8F%99%EC%98%81%EC%83%81</guid>
            <pubDate>Fri, 24 Oct 2025 01:58:55 GMT</pubDate>
            <description><![CDATA[<p>학습에 걸린 총 시간 59.41h</p>
<p>논문에서는 300Epoch까지 진행하였으나 테스트로는 100Epoch까지만 진행.</p>
<p>train 환경 구성</p>
<p>GPU : Geforce 4070 super</p>
<p>anaconda3 가상환경</p>
<p>python : 3.12.3</p>
<p>github의 requirement.txt 설치</p>
<p>pytorch 설치 :  pip3 install torch torchvision torchaudio --index-url <a href="https://download.pytorch.org/whl/cu118">https://download.pytorch.org/whl/cu118</a></p>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/f4490adb-fd2c-4cc7-8228-68be2df73ceb/image.png" alt="">
학습 종료 CLI
<img src="https://velog.velcdn.com/images/chan_woo_00/post/b18bf2dc-ac96-4ee7-bc84-44d592847d7e/image.png" alt="">
학습 결과</p>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/2b6e99aa-e71f-4046-a805-4ad83b586f89/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/503d64e1-cdb6-4096-ad39-afb61b1de28f/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/13743115-e09a-4664-8f98-7aafb78e7a18/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/734998ed-6f0c-40a9-9185-8339e43cad21/image.png" alt=""></p>
<p>논문에서의 결과 (mIoU와 IoU에 대해 동일한 값으로 판단하지 않도록 유의)</p>
<p>A-YOLOM(n) 모델에 대하여 인접하거나 살짝 낮은 결과값을 보임 loss값 추세와 결과물을 확인하였을 때 300Epoch 진행시 논문과 동일한 성능을 가질 것으로 예상된다.</p>
<h4 id="predict-환경-구성">predict 환경 구성</h4>
<blockquote>
<p>anaconda3 가상환경
python : 3.12.3
github의 requirement.txt 설치
pytorch : 2.2.0  + cu118
torchaudio : 2.2.0  + cu118
torchvision : 0.17.0 + cu118</p>
</blockquote>
<p>ultralytics 라이브러리를 pip install로 다운로드 받을 시 sys.path.insert로 경로를 설정해주어도 Anaconda환경에서 먼저 라이브러리를 찾게 되므로 pip install ultralytics 커멘드는 사용하지 말아야 함.</p>
<p>테스트 입력 이미지 사이즈 1280*720 고정</p>
<p>이미지를 Input으로 진행할 때 코드</p>
<pre><code class="language-python">import sys
import torch

sys.path.insert(0, &quot;C:/YOLOv8-multi-task/ultralytics&quot;)

from ultralytics import YOLO

number = 3 #input how many tasks in your work
model = YOLO(&#39;C:/YOLOv8-multi-task/runs/multi/yolopm14/weights/best.pt&#39;)  # Validate the model
model.predict(source=&#39;./img_path&#39;, imgsz=(384,672), device=0,name=&#39;output_path&#39;, save=True, conf=0.25, iou=0.45, show_labels=False, speed=True)</code></pre>
<p>폴더를 경로에 입력해주면 폴더 내부에 있는 모든 이미지에 대해서 예측 작업을 진행해준다.</p>
<p>동영상을 Input으로 진행할 때 코드</p>
<pre><code class="language-python">import cv2
import time
import sys

sys.path.insert(0, &quot;C:/YOLOv8-multi-task/ultralytics&quot;)

from ultralytics import YOLO

number = 3 #input how many tasks in your work
model = YOLO(&#39;C:/YOLOv8-multi-task/runs/multi/yolopm14/weights/best.pt&#39;)  # Validate the model

# Open the video file
video_path = &quot;C:/yolov10/Seoul_30fps.mp4&quot;
cap = cv2.VideoCapture(video_path)

# Get video properties
fps = cap.get(cv2.CAP_PROP_FPS)  # Frame rate
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))  # Frame width
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))  # Frame height

# Define the codec and create VideoWriter object
fourcc = cv2.VideoWriter_fourcc(*&#39;mp4v&#39;)  # Codec for mp4
output_path = &quot;Seoul_predict.mp4&quot;
out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))

black= (0, 0, 0) 
font =  cv2.FONT_HERSHEY_PLAIN

frame_number = 0

# Loop through the video frames
while cap.isOpened():
    # Read a frame from the video
    start_time = time.time()
    success, frame = cap.read()

    if success:
        #Mat객체를 넣어야 하는데 source에는 jpg형태의 객체만 받아들임.
        cv2.imwrite(&quot;./runs/save_frame/frame_&quot; + str(frame_number) + &quot;.jpg&quot;, frame)
        model.predict(source=&quot;./runs/save_frame/frame_&quot; + str(frame_number) + &quot;.jpg&quot;, 
            imgsz=(384,672), device=0, name=&#39;Seoul&#39;, save=True, conf=0.25, iou=0.45, show_labels=False, speed=True)

        print(&quot;C:/YOLOv8-multi-task/runs/multi/Seoul/frame_&quot; + str(frame_number) + &quot;.jpg&quot;)
        annotated_frame = cv2.imread(&quot;C:/YOLOv8-multi-task/runs/multi/Seoul/frame_&quot; + str(frame_number) + &quot;.jpg&quot;)

        cv2.imshow(&quot;YOLO Inference&quot;, annotated_frame)

        frame_number+=1

        end_time = time.time() - start_time
        end_time = f&quot;{end_time * 1000: .2f}&quot;

        annotated_frame = cv2.putText(annotated_frame, &quot;inference and process time :&quot;
                                       + str(end_time) + &quot;ms&quot;, (20, 40), font, 2, black, 1, cv2.LINE_AA)

        # Write the annotated frame to the output video
        out.write(annotated_frame)

        # Display the annotated frame
        #cv2.imshow(&quot;YOLO Inference&quot;, annotated_frame)

        # Break the loop if &#39;q&#39; is pressed
        if cv2.waitKey(10) &amp; 0xFF == ord(&quot;q&quot;):
            break
    else:
        # Break the loop if the end of the video is reached
        break

cap.release()
out.release()
cv2.destroyAllWindows()</code></pre>
<iframe title="YOLOv8_Multi_task 논문 테스트(동영상)" width="640" height="360" src="https://play-tv.kakao.com/embed/player/cliplink/rvkzd3vecikqubtfwojyd8eov@my?service=player_share" allowfullscreen frameborder="0" scrolling="no" allow="autoplay; fullscreen; encrypted-media"></iframe>

<p>불필요한 작업을 진행(저장 -&gt; 호출 -&gt; 저장)하느라 frame당 시간이 오래 걸리는 것을 확인할 수 있다.</p>
<p>frame당 순수한 예측 작업은 평균적으로 50ms 초중반대가 나온다.</p>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/814fa31d-c019-4366-abf8-df4dcb4decf3/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[You Only Look at Once for Real-Time and Generic Multi-Task] Review ]]></title>
            <link>https://velog.io/@chan_woo_00/You-Only-Look-at-Once-for-Real-Time-and-Generic-Multi-Task-Review</link>
            <guid>https://velog.io/@chan_woo_00/You-Only-Look-at-Once-for-Real-Time-and-Generic-Multi-Task-Review</guid>
            <pubDate>Fri, 24 Oct 2025 01:48:16 GMT</pubDate>
            <description><![CDATA[<p>논문의 전체적인 내용은 YOLO model을 사용하여 Multi-Task작업을 수행하는 것이다.</p>
<p>Multi-Task란 직역하면 다중 작업을 의미하며 목적은 3개의 model을 사용하여 진행할 작업을 하나의 model 내에서 작동하도록 하는 것이다.</p>
<p>기본적으로 YOLO model은 하나의 작업(Classification, Detect, Segmentaion, Pose 등)을 목표로 만들어진 모델로 두가지 이상의 작업을 수행하기 위해서는 각각의 작업을 수행하는 모델을 학습시킨 이후 얻은 데이터를 조합하여 최종적인 알고리즘을 구현한다.</p>
<p>이번 논문에서는 하나의 YOLO model에서 여러개의 작업을 수행하여 연산량과 모델의 전반적인 크기를 줄이는 방식을 목표로 한다.</p>
<h2 id="abstract">Abstract</h2>
<ul>
<li><p>자율주행 관련 연구에서 High precision, lightweights, real-time responsiveness는 필수 요건으로 Detection, Segmentation작업을 동시에 처리하는 A-YOLOM 모델을 설계하였다.</p>
</li>
<li><p>Neck과 Backbone 사이의 특징에 대해 능동적(논문에서는 적응적 연결로 소개-adaptively concatenates)으로 특징을 연결하여 학습 가능한 매개변수(가중치)를 사용하고 모든 Segmentation 작업에 대해 동일한 손실 함수를 사용한다.
ㄴ 동일한 손실 함수를 사용한다는 것은 2개의 Segmentation model로 구성되어 손실함수 선택에 대한 고민을 제거하는 것과 동시에 일관된 학습으로 전체적인 모델들에 대한 안전성을 보장함.</p>
</li>
<li><p>Segmentation Head를 Convolutional layer로만 구성되게 하여 파라미터 수와 추론 시간을 줄였다.</p>
</li>
<li><p>BDD100K dataset을 사용하여 기존 모델과 비교하여 긍정적인 결과를 이끌어 내었다.</p>
</li>
</ul>
<h2 id="introduction">Introduction</h2>
<p>ADS(Autonomous driving systems)은 딥러닝의 발전과 함께 집중을 받았고 lane line segmentation, Drivable area segmentation, Object detection 3개의 작업은 ADS에서 핵심 요소로 평가된다.</p>
<p>카메라로 위 3개의 작업을 수행하는 것은 여러가지 자원과 비용면에서 이점이 있으며 자율 주행이라는 특성상 위 작업은 30 FPS를 초과하는 값을 유지하는 것을 기본으로 한다.</p>
<p>경량 모델과 높은 정밀도를 목표로 하는 작업은 Fast R-CNN(two-stage 방식)과 YOLO(one-stage 방식)에서 진행되었고 YOLO에서는 Object Detection에 중점을 두어 발전해 Segmentation Head가 존재하나 사용되는 손실함수나 평가 방식(loss값 계산)들은 Object Detection작업에 최적화된 값들을 사용해왔다. YOLOv8은 하나의 모델에 대해 하나의 작업만을 구현할 수 있으며 여러 모델을 이용하여 알고리즘을 구현하는 것은 학습 시간과 추론 시간 등의 문제점이 존재한다.</p>
<p>Segmentation 작업 특징 
<code>lane line segmentation</code>작업과 <code>Drivable area segmentation</code>에 있어 <code>Drivable area segementation</code>은 이미지의 넓은 영역을 차지하고 주변 상황과 같이 판단하여 High level feature을 필요로 하지만 <code>lane line segmentation</code>은 특징이 길고 단순하게 구성되어 있어 low level feature를 필요로 한다. 이 두 작업을 동시에 하기에는 서로의 정확도에 있어 악영향을 미친다.</p>
<img src=https://velog.velcdn.com/images/chan_woo_00/post/0f155a98-cfa0-4df6-b2b5-a9c6b29d35ff/image.png>

<p>Object detection과 Imege segmentation에 대해서도 Detect작업은 Grid Cell 방식을 사용하고 NMS를 통해 결과를 내보내고 Segmentation 작업은 pixel 단위에서 작동하도록 Decoder를 사용하는 방식을 사용한다.</p>
<p>중점은 두 작업 모두 이미지에서 특징을 추출하는 Backbone단계의 구조는 공유할 수 있는 형태를 가진다는 것이다.</p>
<p>해당 논문에서는 하나의 Backbone과 3개의 Neck, Head를 가지는 Architecture를 사용한다.
<img src=https://velog.velcdn.com/images/chan_woo_00/post/0f2a0ffc-bcde-4b1b-9261-181f38e77e9a/image.png>
하나의 Backbone에서 분할되어 나오는 여러개의 Neck사이에 능동적(P1, P2, P3, P4, P5 각 특징을 원하는 대로 Neck에 연결)으로 모듈을 적용하는 방식으로 하여 서로 다른 레벨의 특징을 연결할지 여부를 결정할 수 있다.</p>
<h2 id="methodology">Methodology</h2>
<p>A-YOLOM 모델은 Encoder-Decoder Architecture를 가진 one-stage 네트워크로 Encoder는 Backbone과 Neck로 구성되었으며 Decoder는 Head로 구성된다.</p>
<p>하나의 Backbone과 세 가지 작업을 위한 3개의 Neck, Head를 단일 모델로 통합한다.</p>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/0ebf2c19-70a2-4193-87b8-09ecc7c03ad5/image.png" alt="">
(N 값은 Neck의 수로 해당 논문에서는 2개를 의미한다.)</p>
<h3 id="encoder-backbone_neck">Encoder (Backbone_Neck)</h3>
<h4 id="backbone">Backbone</h4>
<p>기존 YOLOv5에서 사용되던 Backbone인 SCP-Darknet53을 개선.</p>
<p>YOLOv8의 기초 Backbone과 동일하다.</p>
<h4 id="neck">Neck</h4>
<p>3개의 Neck을 활용 { lane lines, drivable areas, object detection }</p>
<p>Neck의 구조는 동일하게 사용하나(그림의 N이 2) 각 목적이 다르게 사용되여 가중치의 값들은 상당히 다르게 진행.</p>
<p>Object Detection 작업의 경우 차량을 감지하여야 하므로 low-level feature보다는 high-level feature의 특징을 조합하여야 하므로  Backbone에서 P3~P5까지의 mid-level과 high-level들을 조합하여 사용하였고 이 과정에서 FPN을 포함하는 PAN구조를 채택하였다. PAN 구조를 사용함으로서 작은 객체와 큰 객체에 대한 정확도를 향상시킨다.</p>
<p>Image Segmentation 작업의 경우 low-level feature과 high-level feature정보를 모두 포함하도록 P1~P5까지의 특징들을 Backbone에서 불러온다. 하지만 lane line에서 high-level feature이 크게 의미가 없는 특징맵의 경우 Adaptive Concatenation Module(AC)을 통해 해당 Feature은 연결하지 않는 등의 작업으로 연산량을 줄이고 정확도를 향상시킨다.</p>
<p>Adaptive Concatenation Module의 알고리즘</p>
<img src=https://velog.velcdn.com/images/chan_woo_00/post/ed81afa5-5c3a-4207-a22a-bb079425c1c1/image.png>
X라는 텐서에 대해  x[0]는 neck에서 Upsampling과정을 통해 확장된 특성맵을 의미하며 x[1]은 Backbone에서 확장된 특성맵과 동일 해상도의 특성맵을 Input으로 사용한다.



<p>Output과정은 초기값 weight(5.0)을 기반으로 특성맵을 연결할지 말지에 대한 여부를 결정한다.</p>
<p>weight값은 sigmoid함수의 파라미터로 사용되어 해당 값이 0.5를 초과하게되면 두 특성맵을 결합하고 1*1 Convolution layer를 통과시켜 채널 수(차원)를 줄이는 과정을 수행한다. 0.5 이하의 값이 나오면 Neck 특성맵만을 C2f 에 통과시킨다.</p>
<p>5번 라인을 간단히 살펴보면 Conv는 Ultralytics에서 제공하는 함수를 의미하며 내부 파라미터는 차례대로 [입력 채널 크기, 출력 채널 크기, kernel size, stride]이다.</p>
<p>학습이 진행되며 weight값이 커져 sigmoid함수를 통과시킬 때 0.5가 넘게 되면 해당 특성맵을 연결하는 과정을 통해 lane line에서는 연결되지 않은 특성맵이 drivable area에서는 연결되는 등의 작업이 이루어질 수 있다.</p>
<p>해당 과정에서 Segmentaion의 Neck를 분리시킨 이유가 나온다. (하나이면 해당 과정에 의미가 약해진다.)</p>
<p>위 과정은 각 작업의 Neck과정에서 불필요한 연산을 줄이며 정확도를 높이게 된다.</p>
<ul>
<li>Detection과 Segmentation의 Neck 구조에서 가장 다른 점은 FPN에서 끝나는 Segmentation Neck과는 다르게 Detection Neck에서는 이를 포함하는 PAN network를 사용한다는 것인데 이유는 차량 객체의 경우 멀리 있을 때와 가까이 있을 때의 객체 크기에 상당한 차이를 보이는 반면 lane lines, drivable areas의 경우 멀리 있을 때와 가까이 있을 때의 차이가 크지 않아 민감하게 반응할 이유가 없기 때문에 연산량을 보존하고자 FPN에서 끝낸다. (개인적 의견 첨가)-</li>
</ul>
<h4 id="decoder-head">Decoder (head)</h4>
<p>Backbone과 Neck에서의 작업을 처리하여 예측을 수행 2개의 Segment Head와 1개의 Detect Head를 사용</p>
<p>Head</p>
<img src=https://velog.velcdn.com/images/chan_woo_00/post/8765e70a-e820-4786-a34d-58dd753db705/image.png>
Detect Head는 YOLOv8의 기본 Head와 동일하다.

<img src=https://velog.velcdn.com/images/chan_woo_00/post/f5a96091-746a-4913-8ce0-8668d93b8a74/image.png>
Segmentation Head는 Conv레이어와 DeConv레이어로 이루어져 있는데 DeConv레이어는 Upsampling과 같은 역할로 해상도를 높이나 알고리즘 측면에서 다르다. Upsampling은 하나의 픽셀의 값을 복사하여 넓히거나 사이값을 넣어서 해상도 자체를 늘리는데 집중한다면 Deconv는 가중치를 포함하여 특성맵의 정보들을 파악하며 원본의 해상도와 객체의 경계면 정보들을 확보한다.


<img src=https://velog.velcdn.com/images/chan_woo_00/post/ea9d6a69-f3c1-406e-a945-aac8d8b37893/image.png>
Segment Head의 알고리즘으로 특성맵을 받아 처음 출력 채널 수 32로 하여 중간 레이어의 채널 수를 고정한다.

<p>cv3(cv2(upsample(cv1(x)))을 거치면 해상도가 입력 특성맵의 최종적으로 2배 증가하게 된다.</p>
<ul>
<li>입력 특성맵으로 사용하는 P1의 해상도 크기 기준 2배.</li>
</ul>
<h4 id="loss-function">Loss Function</h4>
<p>해당 모델에서 손실함수는 다음과 같이 정의된다.</p>
<img src=https://velog.velcdn.com/images/chan_woo_00/post/98cd2307-ea1c-4556-859b-4eb5920793ce/image.png>

<p>순서대로 Detect, Seg draivable area, Seg lane line 에 대한 손실이다.</p>
<p>객체 탐지(L_det)에서는 <a href="https://velog.io/@d4r6j/FL-vs-DFL">DFL loss</a>(Distribution Focal Loss), <a href="https://curt-park.github.io/2018-09-19/loss-cross-entropy/">BCE loss</a>(Binary Cross Entropy), <a href="https://wikidocs.net/163050">CIoU loss</a>를 조합하여 계산하는데 각각 class, Bbox, Bbox를 계산하는데 사용된다. 위 방식은 YOLOv8의 일반적인 객체 탐지 방법과 동일한 방식이다.</p>
<p>Seg loss에 대해서는 아래와 같이 정의된다.</p>
<img src=https://velog.velcdn.com/images/chan_woo_00/post/e413f418-b54c-4b43-9d3d-97dc9cb3df66/image.png>
두 작업에 대한 공식은 동일하며 λ은 가중치로 바로 뒤에 나오는 L_FL과 L_TL에 대한 중요도를 가중치로 나타낸다. 해당 값은 하이퍼 파라미터로 지정하는 방식과 학습 도중 조정하는 방법이 있다.

<p>L_FL은 <a href="https://woochan-autobiography.tistory.com/929">Focal loss</a>를 의미하고 L_TL은 <a href="https://modulabs.co.kr/blog/machine_learning_loss_function">Tversky loss</a>를 의미한다. ( Tversky loss는 Segmentation에서만 쓰이는 손실 함수로 명확히 설명되어있는 사이트를 찾지 못함.)</p>
<p>각 손실함수에 대한 역할에 대해서 간단히 집고 넘어가자면 Focal loss는 데이터 불균형과 데이터의 난이도에 따른 학습 정도(어느정도로 가중치를 민감하게 수정할지)를 조정하며 Tversky loss는 객체에 대한 경계면을 세세하게 구분하기 위한 손실함수이다.</p>
<p>Experiment</p>
<p>사용한 데이터셋 : BDD100K (자동차, 버스, 트럭 등의 객체들을 Vehicle로 통합)</p>
<p>평가 방식 : mAP50(객체 탐지), mIoU(Drivable area), IoU + Acc(lane line)</p>
<p>사용 장비 : Train 과정 RTX 4090  3개 - Val과정 GTX 1080 Ti GPU 1개로 명시</p>
<p>데이터 증식 기법 : Ultralytics 기본 데이터 증식 기법 사용.</p>
<p>실험 방식 : SGD optimizer, learning rate : 0.01, momentum : 0.937, weight decay : 0.0005, NMS : 0.6</p>
<ul>
<li>3 Epoch까지는 워밍업으로 learning rate값등이 올라간 상태로 시작함. 이미지 사이즈 640*640</li>
</ul>
<p>평가에 관련하여 multi-task관련 모델(평가를 비교할)들이 부족하여 객체 검출 모델과 영상 분할 모델들을 같이 사용하여 테스트하였다.</p>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/82e1147a-0c39-4e07-bb16-9085590efe81/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/ae2c150a-fbe5-4a5f-82ef-c9dda19d64d5/image.png" alt="">
<img src="https://velog.velcdn.com/images/chan_woo_00/post/c49c506a-45ef-4d91-8e12-607333e222e1/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[COCO dataset]]></title>
            <link>https://velog.io/@chan_woo_00/COCO-dataset</link>
            <guid>https://velog.io/@chan_woo_00/COCO-dataset</guid>
            <pubDate>Wed, 22 Oct 2025 09:52:36 GMT</pubDate>
            <description><![CDATA[<p>COCO dataset은 80개의 class로 구성되어 Computer vision 관련 모델을 학습하고 검증하는데 사용되는 대표적인 dataset이다.</p>
<p>총 33만개의 이미지가 포함되고 20만개의 이미지에 Object detection, Segmentationl, Captioning(그림을 단어로 설명하는 작업)에 대한 주석이 포함되어있다.</p>
<p>주석에는 Bounding box, Segmentation mask, Caption in image에 대한 정보가 포함되어있다.</p>
<p>COCO dataset은 &quot;Train2017&quot;, &quot;Val2017&quot;, &quot;Test2017&quot; 3가지의 디렉리로 구성되어 있는데 Train에는 11.8만의 이미지가 포함되고 Val dataset은 5천개의 이미지가 있으며 Test에는 2만개의 이미지로 구성되어 있다. Test dataset에 대한 주석은 제공되지 않고 Test dataset에 대한 구체적인 성능 평가를 받기 위해서는 <a href="https://codalab.lisn.upsaclay.fr/competitions/7384">COCO eval‎uation server</a>에 제공해야 한다.</p>
<p>Ultralytics에서는 COCO dataset에 대한 구성으로 YAML(야뮬)파일을 사용하며 데이터세트의 경로, 클래스, 기타 정보들을 포함한다.</p>
<p><a href="https://github.com/ultralytics/ultralytics/blob/main/ultralytics/cfg/datasets/coco.yaml">https://github.com/ultralytics/ultralytics/blob/main/ultralytics/cfg/datasets/coco.yaml</a></p>
<pre><code># Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license

# COCO 2017 dataset https://cocodataset.org by Microsoft
# Documentation: https://docs.ultralytics.com/datasets/detect/coco/
# Example usage: yolo train data=coco.yaml
# parent
# ├── ultralytics
# └── datasets
#     └── coco  ← downloads here (20.1 GB)

# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
path: ../datasets/coco # dataset root dir
train: train2017.txt # train images (relative to &#39;path&#39;) 118287 images
val: val2017.txt # val images (relative to &#39;path&#39;) 5000 images
test: test-dev2017.txt # 20288 of 40670 images, submit to https://competitions.codalab.org/competitions/20794

# Classes
names:
  0: person
  1: bicycle
  2: car
  3: motorcycle
  4: airplane
  5: bus
  6: train
  7: truck
  8: boat
  9: traffic light
  10: fire hydrant
  11: stop sign
  12: parking meter
  13: bench
  14: bird
  15: cat
  16: dog
  17: horse
  18: sheep
  19: cow
  20: elephant
  21: bear
  22: zebra
  23: giraffe
  24: backpack
  25: umbrella
  26: handbag
  27: tie
  28: suitcase
  29: frisbee
  30: skis
  31: snowboard
  32: sports ball
  33: kite
  34: baseball bat
  35: baseball glove
  36: skateboard
  37: surfboard
  38: tennis racket
  39: bottle
  40: wine glass
  41: cup
  42: fork
  43: knife
  44: spoon
  45: bowl
  46: banana
  47: apple
  48: sandwich
  49: orange
  50: broccoli
  51: carrot
  52: hot dog
  53: pizza
  54: donut
  55: cake
  56: chair
  57: couch
  58: potted plant
  59: bed
  60: dining table
  61: toilet
  62: tv
  63: laptop
  64: mouse
  65: remote
  66: keyboard
  67: cell phone
  68: microwave
  69: oven
  70: toaster
  71: sink
  72: refrigerator
  73: book
  74: clock
  75: vase
  76: scissors
  77: teddy bear
  78: hair drier
  79: toothbrush

# Download script/URL (optional)
download: |
  from ultralytics.utils.downloads import download
  from pathlib import Path

  # Download labels
  segments = True  # segment or box labels
  dir = Path(yaml[&#39;path&#39;])  # dataset root dir
  url = &#39;https://github.com/ultralytics/assets/releases/download/v0.0.0/&#39;
  urls = [url + (&#39;coco2017labels-segments.zip&#39; if segments else &#39;coco2017labels.zip&#39;)]  # labels
  download(urls, dir=dir.parent)
  # Download data
  urls = [&#39;http://images.cocodataset.org/zips/train2017.zip&#39;,  # 19G, 118k images
          &#39;http://images.cocodataset.org/zips/val2017.zip&#39;,  # 1G, 5k images
          &#39;http://images.cocodataset.org/zips/test2017.zip&#39;]  # 7G, 41k images (optional)
  download(urls, dir=dir / &#39;images&#39;, threads=3)</code></pre><p>기본적으로 YOLO CLI를 사용하여 훈련을 진행하는 경우에는 따로 다운로드 받을 필요 없이 YAML파일에 저장된 내용대로 경로를 찾아 dataset이 없다면 다운로드 받고 있다면 그대로 사용한다.</p>
<p>YOLO dataset을 다운받게 되면</p>
<p>다음과 같이 디렉터리가 구성되는데 coco 디렉터리 내부에 annotations와 images가 생성된다.
annotations는 주석이 달려있는 json파일이 존재하는 파일로 해당 json파일을 YOLO형식에 맞게 수정하면 labels 디렉터리가 생기게 된다.(json2yolo)
(YOLO CLI 사용시 해당 과정 불필요)
coco dataset format을 살펴보게 되면 annotations 디렉터리에는 아래 그림과 같이</p>
<p>&lt; captions _Tr_Val, instances_Tr_Val, poss _Tr_Val &gt; 로 구성되어 있다.
<img src=https://velog.velcdn.com/images/chan_woo_00/post/b7690383-2304-4b3d-9ab4-efc98dcfd31c/image.png></p>
<img src=https://velog.velcdn.com/images/chan_woo_00/post/bdee2b31-64fb-49d4-a67d-f81d0d42700e/image.png>
annotation은 json형식의 파일로 내부를 살펴보면 처음 다운로드 한 파일에서는 하나의 줄에 모든 정보가 들어가 있어 VScode 같은 IDE에서는 val같이 데이터 수가 적은 파일만 살펴볼 수 있는 등의 문제가 있어 해당 json파일을 보기 좋게 줄을 나누어주는 과정이 필요하다.(내부 데이터를 확인하지 않고 사용만 하려면 불필요함.)

<p>해당 과정은 github를 살펴보아도 나오지만 jq를 사용하는 방식을 추천한다.
<a href="https://nepersica.tistory.com/22">https://nepersica.tistory.com/22</a></p>
<p>다음 과정을 거치면 다음과 같이 줄바꿈이 진행되어 새롭게 파일이 저장된다.
<img src="https://velog.velcdn.com/images/chan_woo_00/post/a920c445-5a64-403b-8880-28e7ca8f804a/image.png" alt=""></p>
<table>
<thead>
<tr>
<th>변수 명</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>license</td>
<td>이미지의 라이센스에 대한 숫자 코드</td>
</tr>
<tr>
<td>file_name</td>
<td>파일의 이름</td>
</tr>
<tr>
<td>coco_url</td>
<td>coco dataset 서버에서 해당 이미지가 저장된 링크</td>
</tr>
<tr>
<td>height</td>
<td>이미지의 높이(세로 길이)</td>
</tr>
<tr>
<td>width</td>
<td>이미지의 너비(가로 길이)</td>
</tr>
<tr>
<td>data_captured</td>
<td>이미지가 촬영된 날짜</td>
</tr>
<tr>
<td>flickr_url</td>
<td>이미지가 업로드된 flickr라는 사이트의 링크</td>
</tr>
<tr>
<td>id</td>
<td>coco dataset에서의 고유 식별 번호</td>
</tr>
</tbody></table>
<p>후반부에는 이미지에 대한 annotaions을 제공한다. Bbox와 segmentation points등
<img src="https://velog.velcdn.com/images/chan_woo_00/post/25591611-07dd-488b-83ec-0ebd19cd8cb2/image.png" alt=""></p>
<pre><code>{
      &quot;segmentation&quot;: [
        [
          260.4,
          231.26,
          215.06,
          274.01,
          194.33,
          307.69,
          195.63,
          329.72,
          168.42,
          355.63,
          120.49,
          382.83,
          112.71,
          415.22,
          159.35,
          457.98,
          172.31,
          483.89,
          229.31,
          504.62,
          275.95,
          500.73,
          288.91,
          495.55,
          344.62,
          605.67,
          395.14,
          634.17,
          480,
          632.87,
          480,
          284.37,
          404.21,
          223.48,
          336.84,
          202.75,
          269.47,
          154.82,
          218.95,
          179.43,
          203.4,
          194.98,
          190.45,
          211.82,
          233.2,
          205.34
        ]
      ],
      &quot;area&quot;: 108316.66515000002,
      &quot;iscrowd&quot;: 0,
      &quot;image_id&quot;: 520301,
      &quot;bbox&quot;: [
        112.71,
        154.82,
        367.29,
        479.35
      ],
      &quot;category_id&quot;: 18,
      &quot;id&quot;: 3186
    },</code></pre><table>
<thead>
<tr>
<th>변수 명</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>segmentation</td>
<td>경계를 찍은 points 정보</td>
</tr>
<tr>
<td>area</td>
<td>객체의 면적 (Segmentation으로 Bounding box의 면적은 아니다.)</td>
</tr>
<tr>
<td>iscrowd</td>
<td>객체가 하나로 연결되어있는지 아닌지에 대한 정보, 하나의 객체가 가려져 분리되어 있다면 1로 표시되고 연결이 잘 되어있다면 0으로 표시된다.</td>
</tr>
<tr>
<td>image_id</td>
<td>이미지의 고유 번호</td>
</tr>
<tr>
<td>bbox</td>
<td>객체를 둘러싸는 Bounding Box의 정보로 순서대로 Bounding Box의 중심좌표 (x,y)와 중심 좌표를 기준으로 하는 width, height정보를 가진다.</td>
</tr>
<tr>
<td>category_id</td>
<td>객체의 분류 카테고리 번호</td>
</tr>
<tr>
<td>id</td>
<td>객체 주석의 고유 번호</td>
</tr>
</tbody></table>
<p>마지막 영역에는 categorise 정보가 들어간 내용으로</p>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/e302cfa0-1197-4370-bfc0-eccf3bd6966e/image.png" alt="">
supercategory는 상위 개념의 클래스를 의미하고 내부 name에서 하위 개념의 클래스를 구별해준다.</p>
<p>categories에서 id는 클래스의 번호를 의미한다. (coco.yaml)의 순서와 동일함.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[YOLOv10: Real-Time End-to-End Object Detection] Test. ]]></title>
            <link>https://velog.io/@chan_woo_00/YOLOv10-Test</link>
            <guid>https://velog.io/@chan_woo_00/YOLOv10-Test</guid>
            <pubDate>Wed, 22 Oct 2025 09:37:18 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>논문에서 소개한 성능표</p>
</blockquote>
<img src=https://velog.velcdn.com/images/chan_woo_00/post/868674ee-5b6e-4aa5-ab5b-3680eeff0e6c/image.png>
AP값을 어떠한 기준으로 가져왔는지는 정확히 나와있는 사이트는 확인하지 못하였으나 Ultralytics Community(Discord)에 의하면 통상적으로 논문에서 제시하는 AP값은 특정 코멘트가 없는 이상 AP(0.5:0.95)를 기준으로 한다고 한다. 확인을 위해 몇가지 버전의 모델들을 다운받아 테스트 한 결과 AP(0.5:0.95)값들에 대해 거진 같은 값(Tensor RT 등의 작업으로 인한 오차 감안)을 가지는 것을 확인할 수 있었다.  (Toxite 유저는 Community Helper 역할 부여받음)

<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/101b7d72-ff2a-4524-9582-fd29164b7a39/image.png" alt=""></p>
<p>yolov10l.pt , yolov10n.pt, yolov10s.pt 지표</p>
<img src=https://velog.velcdn.com/images/chan_woo_00/post/edce2b15-7b2f-484b-85b2-b3de40bdd8a1/image.png>
<img src=https://velog.velcdn.com/images/chan_woo_00/post/c3b7d373-0e22-416d-815c-d275341027ba/image.png>
<img src=https://velog.velcdn.com/images/chan_woo_00/post/5b1b87d8-322d-4eb6-8e2a-86a4561c7b6b/image.png>


<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/558f1ae1-4e99-4c09-b664-ce49861829b8/image.png" alt="">
100epoch 훈련 결과 AP(0.5:0.95)val값은 0.431로 출력 </p>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/ff66aadf-722f-4c5d-abcd-d5c7dac6d554/image.png" alt=""></p>
<p>논문과 one-to-one Head에 대해서 0.032(3.2%)의 차이가 존재하는 것을 확인. 해당 값의 차이를 확인하기 위해서 논문을 다시 한번 확인해보기로 하고 체크하였을 때 차이점의 여부에 대해 확인할 수 있었다.</p>
<p>다른 조건들은 모두 동일했으나 Epoch수에 대해 차이점이 있었다.</p>
<p>기본 실험에서는 Ultralytics의 Github를 소개하며 해당 사이트와 동일한 조건으로 테스트하였다고 소개하였으나 세부 영역에서 논문에서는 총 500 Epochs를 돌렸고(3090GPU-8 EA로 학습을 진행) 400 Epochs에 대한 차이값이 0.032의 AP(0.5:0.95)차이점을 나타낸 것으로 확인된다.</p>
<img src=https://velog.velcdn.com/images/chan_woo_00/post/907a0522-210f-4737-b75c-a85c75157e86/image.png>

<img src=https://velog.velcdn.com/images/chan_woo_00/post/e36879f4-3966-40ca-b380-9ffc9f6e41f8/image.png>

<p>500Epoch를 돌리려면 예상 시간으로 150시간정도가 걸릴 것으로 예상되어 테스트는 잠시 미뤄두기로 하였으나 값이 감소되는 추세를 보았을 때 Epoch를 늘렸을 때 0.46까지 도달할 것으로 예상된다.
<img src="https://velog.velcdn.com/images/chan_woo_00/post/6999e43c-7c3b-42fa-999a-cd1bd8d10792/image.png" alt="">
붉은색 라인이 AP(0.5:0.95)영역에 대한 값으로 0.4294에 대한 AP값을 출력.</p>
<p>해당 커맨드는 coco dataset에 대해서  best.pt모델을 사용하여 val을 진행한 과정.
<img src="https://velog.velcdn.com/images/chan_woo_00/post/b2d05725-04c9-4085-a079-1c839c802498/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[YOLOv10_train과 val의 파라미터 차이 ]]></title>
            <link>https://velog.io/@chan_woo_00/YOLOv10parameter</link>
            <guid>https://velog.io/@chan_woo_00/YOLOv10parameter</guid>
            <pubDate>Wed, 22 Oct 2025 09:23:01 GMT</pubDate>
            <description><![CDATA[<p>YOLOv10모델을 사용하다 보면 논문에 나온 Param와 실제 테스트시 차이가 존재하는 것을 확인할 수 있다.</p>
<blockquote>
<p>학습 CLI</p>
</blockquote>
<img src=https://velog.velcdn.com/images/chan_woo_00/post/ff067ce8-fe05-460d-9646-e69303cf3092/image.png aling='left'>

<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/8d983a59-ed06-4874-b82b-844f71ac6acf/image.png" alt="">
학습 시에는 약 8.1정도의 Param.(M)의 값이 나오나 논문에서는 7.2의 Param.(M)를 가지고 있다고 표시한다.</p>
<p>해당 값들의 차이는 one-to-many의 파라미터의 차이로 YOLOv10에서는 추론시 학습이 완료된 one-to-one Head의 파라미터에 의존하여 추론을 하기 때문에 one-to-many Head의 파라미터를 사용하지 않는다.</p>
<pre><code class="language-python">from ultralytics import YOLOv10

model = YOLOv10(&#39;best.pt&#39;)
model.model.model[-1].export = True
model.model.model[-1].format = &#39;onnx&#39;
del model.model.model[-1].cv2
del model.model.model[-1].cv3
model.fuse()</code></pre>
<p>따라서 학습이 완료된 모델에 대해 파라미터를 출력하게 되면 논문에 제시되어있는 파라미터가 출력되는 것을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/chan_woo_00/post/e58a5505-ad9e-4b9a-a9f6-bdd92f680445/image.png" alt=""></p>
<p><a href="https://github.com/THU-MIG/yolov10/issues/178">https://github.com/THU-MIG/yolov10/issues/178</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[YOLO의 Neck영역과 PAN]]></title>
            <link>https://velog.io/@chan_woo_00/PAN</link>
            <guid>https://velog.io/@chan_woo_00/PAN</guid>
            <pubDate>Wed, 22 Oct 2025 09:11:33 GMT</pubDate>
            <description><![CDATA[<img src=https://velog.velcdn.com/images/chan_woo_00/post/ba2bf90e-2a4b-4051-9509-5b12edf516af/image.png>
이전 YOLOv10의 Architecture에 대해 공부하다가 해당 이미지에 대해 PAN이 하는 역할에 대해 조사하던 중 추가적으로 정리할만한 가치가 있다 판단하였다.


<p><code>Regression Head</code> : 회귀 헤드라고 불리며 Bounding Box를 계산하는데 사용된다.
YOLO에서는 Onjectness Score(객체가 존재할 확률)를 Regression Head에 포함하여 쓰이는 경우가 대부분이며 YOLOv8에서는 Anchor를 사용하여 계산하고 Anchor를 사용하지 않는 YOLOv10의 경우에는 객체의 중심 좌표와 크기를 one-to-one Head에서 출력된 정보와 비교하며 Bounding Box를 계산한다.</p>
<p><code>Classification Head</code> : 분류 헤드라고 불리며 해당 객체에 대한 Class를 예측하고 학습하는 Head이다.
Detection 작업에서는 예측된 Bounding Box 내에 어떤 Class가 포함되어있는지를 하습하며 클래스에 대한 신뢰도 점수를 기반으로 예측한다.</p>
<p>두 헤드는 각각의 예측과 오류값을 기반으로 동시에 학습된다.</p>
<img src=https://velog.velcdn.com/images/chan_woo_00/post/01173f79-86d5-4caa-9c19-e0ba8f4c6076/image.jpg>

<p><a href="https://www.slideshare.net/slideshow/ndc-2016-61452271/61452271">회귀 분류 이미지</a></p>
<p><a href="https://www.youtube.com/watch?v=y1dBz6QPxBc&amp;list=PL1Kb3QTCLIVtyOuMgyVgT-OeW0PYXl3j5&amp;index=8">관련 영상</a></p>
<p>PAN은 Path Aggregation Network의 약자로 본래는 Instant Segmentation모델에 대해 제작되었지만 성능이 좋아 여러 AI모델에서 기용하고 있다.</p>
<p>YOLOv3와 yolov8의 모델 구조는 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/027fd13e-12ee-4e79-ba19-5ed8575ad855/image.png" alt="">
<img src="https://velog.velcdn.com/images/chan_woo_00/post/b50f7552-25f2-4638-bc63-ac4e015af13f/image.png" alt=""></p>
<p>해당 두 Architecture에서 자잘한 레이어들에 대한 차이점은 물론 존재하지만 직접적으로 보이는 가장 큰 차이점은 Neck영역으로 구성되는 파트이다.</p>
<p>YOLOv3의 경우 FPN(Feature Pyramid Networks)를 사용하고 있고 YOLOv8의 경우 FPN을 개량한 PAN(Path Aggregation Network)을 사용하고 있다.</p>
<p>FPN은 <a href="https://arxiv.org/pdf/2303.13043v2">Top-Down</a>을 기반으로 상위 계층에서 하위 계층으로 Conv레이어를 거치며 레이어가 합쳐지고 PAN은 기존의 FPN구조에 Bottom-Up을 기반으로 하는 구조가 합쳐저서 구성되어 있다.</p>
<p>설명 이전에 Backbone에서 Bottom-Up과정이 수행되지만 각 특정 레이어(Yolov8의 예시에서 4, 6, 9 layer)에서 특징맵을 가져오기 때문에 해당 특징들에 대한 조합이 이루어지지 않는다. 해당 과정을 Neck에서 수행해준다.</p>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/21283f42-0119-4a00-b1e0-4ab835b70941/image.png" alt="">
두 구조에 대한 차이점을 직관적으로 이야기하면 Head가 받는 특징맵 정보의 질의 차이이다.</p>
<p>FPN을 사용하는 YOLOv3의 경우 Backbone에서 출력된 레이어에 대해 한번의  Conv과정을 통해 low-level feature(edge, color)을 조합한 high-level feature을 학습하게 되된다. </p>
<p>PAN을 사용하는 YOLOv8의 경우 SPPF에서 출력된(YOLOv3를 예시로 들면 하늘색 영역의 Neck) 특징맵이 한번 FPN구조를 따라간 다음 다시 Bottom-Up구조를 거치며 low-level feature를 받게되어 high-level의 특징맵을 가진 20<em>20</em>512*w사이즈를 받는 Head에 대해서도 강한 low-level feature의 정보를 가지게 된다.</p>
<p>예시를 들게 된다면 FPN은 문제집 하나를 푸는 것과 같고 PAN은 문제집 하나를 풀고 여러 유형을 결합한 문제집를 다시 한번 푸는 것과 같다.</p>
<p><a href="https://arxiv.org/pdf/1803.01534">PANet</a>( Path Aggregation Network for Instance Segmentation )</p>
<p>신경망의 정보를 Head로 전달하는 과정을 개선하기 위한 논문.
<img src="https://velog.velcdn.com/images/chan_woo_00/post/4c875368-c2bb-46fd-ac7d-3876aeeaa823/image.png" alt=""></p>
<p>PAN을 설명하기 위한 구조로 (a)영역에 대해 FPN Backbone으로 칭하고 (b)영역에 대해 <code>Bottom-up path augmentaion</code>작업을 수행한다.</p>
<p><a href="https://paperswithcode.com/method/bottom-up-path-augmentation">Bottom-up path augmentation</a></p>
<p>FPN에서 바로 Head로 연결되면 low-level featrue의 정보를 Big object에서 직접적으로 활용하기 힘들기 때문에 각 특징의 특성을 직접 연결하는 방식으로 여러 해상도에 대해서 low-level feature과 high-level feature을 사용할 수 있게 한다.</p>
<p>해당 논문에서는 FPN에서 생성한 레이어들의 해상도(P5, P4, P3, P2)에 맞춰 같은 크기의 해상도를 가지는 특징맵(N5, N4, N3, N2)들을 구성한다. P2와 N2는 동일한 특징맵이다. 
<img src=https://velog.velcdn.com/images/chan_woo_00/post/1d96f216-ae0e-4283-a4fd-961ba028a9b1/image.png>
N_n 특징맵들은 3*3_Conv작업을 거치며 해상도가 낮아진 다음 P(n+1) 특징맵들과 Concat레이어를 통해 합쳐지며 새로운 특징맵 N(n+1)을 생성한다. 해당 과정을 반복하며 최종적으로 N5특징 맵을 가지게 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[YOLOv10: Real-Time End-to-End Object Detection] Review]]></title>
            <link>https://velog.io/@chan_woo_00/YOLOv10-Real-Time-End-to-End-Object-Detection-Review-0925vwy1</link>
            <guid>https://velog.io/@chan_woo_00/YOLOv10-Real-Time-End-to-End-Object-Detection-Review-0925vwy1</guid>
            <pubDate>Tue, 21 Oct 2025 11:50:18 GMT</pubDate>
            <description><![CDATA[<p>논문 분석 및 용어 정리 글(설명이 깊게 들어가야 하는 내용들은 링크로 연결).</p>
<h2 id="yolov10-real-time-end-to-end-object-detection">YOLOv10: Real-Time End-to-End Object Detection</h2>
<p>직역해 보자면 YOLOv10 : 실시간 객체 검출기(End-to-End 의 형태를 가진)이다.</p>
<p>간단하게 End-to-End의 용어를 집고 넘어가자면 객체의 특징 추출부터 추론까지의 과정을 하나의 Architecture 내에서 진행하는 모델의 구조를 의미한다.</p>
<img src=https://velog.velcdn.com/images/chan_woo_00/post/9891a093-ff33-4c0f-b355-7da7f00c534c/image.png>

<p>위 <a href="https://velog.io/@jeewoo1025/What-is-end-to-end-deep-learning">이미지</a>는 이전의 전통적으로 진행되던 딥러닝 모델에서는 데이터가 들어오면 그에 대한 특징 추출과 특징에 대한 추정 최종 결과까지 여러 단계의 파이프라인을 지나치게 되며 OUTPUT값을 보내지만 End-to-End 구조의 경우에는 이전에 나누어지던 파이프라인들을 하나의 딥러닝 네트워크 내에서 진행한다. 통상적으로 end2end로 적기도 한다.</p>
<h3 id="abstract">Abstract</h3>
<p>이전 YOLO 모델부터 이미지 처리를 위해서 컴퓨팅 자원 소모와 처리 시간에 대한 감소를 목적으로 계속해서 발전해왔지만 YOLOv8의 단계까지 왔음에도 해결하지 못한 몇몇 문제점이 존재하는데 해당 논문에서는 후처리 및 모델 아키텍처에서의 YOLO 성능과 효율성의 단계를 증진시키는것을 목적으로 작성되었다.</p>
<p>논문에서 제시한 YOLOv10에서 이전의 YOLO 시리즈들과 대비되어 해결한 내용들은 아래와 같다.</p>
<ol>
<li><p>객체를 탐지하고 후처리 과정에서 진행해야 하는 NMS(Non-Maximum-suppression)</p>
</li>
<li><p>특징(feature)을 추출하는 단계에서 모델의 크기가 증가할수록 증가하게 되는 연산 오버헤드(Computational Overhead)</p>
</li>
<li><p>정확도와 효율성에 대한 개선</p>
</li>
</ol>
<p>결과적으로는 YOLOv10은 이전 버전 혹은 다른 모델과 비교하였을 때 하나의 INPUT값에 대해 적은 수의 파라미터와 낮은 수의 FLOPs과 Latency를 가진다.</p>
<p>FLOPs(Floating point Operations) : 부동소수점 연산을 의미하며 여기에서 연산은 사칙연산,  log, exp등의 연산이 포함된다.</p>
<p>-FLOPs는 동일 성능 대비 낮을수록 좋다고 생각할 수 있다.(해당 모델에서 소비하는 컴퓨팅 자원이 적다는 것을 의미함)</p>
<p>Latency : 직역은 대기시간으로 딥러닝 추론의 시작부터 완료까지 걸리는 시간을 의미한다, Latency(f)가 붙은 것들은 이미지 후처리에 대한 시간을 제외한 나머지 시간을 의미한다.</p>
<h3 id="introduction">Introduction</h3>
<p>소개에서는 이제동안 YOLO시리즈가 발전해온 과정과 NMS가 왜 문제가 되는지에 대해서 소개해준다.</p>
<p>NMS는 Non-Maximum-suppression의 약자로 YOLOv8을 기준으로 이미지에서 예측되는 Bounding Box를 모두 특정 임계값에 대해 일차적으로 처리를 하였을 때 남게되는 중복되는 Bounding Box들이 있다.</p>
<table>
<thead>
<tr>
<th>NMS 미적용</th>
<th>NMS적용</th>
</tr>
</thead>
<tbody><tr>
<td><img src=https://velog.velcdn.com/images/chan_woo_00/post/089e874a-0e33-4b94-85ec-8a54c42ff010/image.png></td>
<td><img src=https://velog.velcdn.com/images/chan_woo_00/post/feaa34fc-b851-4f4e-891e-d11a649fa3bc/image.png></td>
</tr>
</tbody></table>
<p>위 <a href="https://www.analyticsvidhya.com/blog/2020/08/selecting-the-right-bounding-box-using-non-max-suppression-with-implementation/">이미지</a>를 보았을  때 각 Bounding Box들이 하나의 객체에 대해서 여러개의 후보군들이 있는데 이 때 NMS를 통하여 우측의 사진처럼 하나의 객체에 하나의 Bounding Box만이 남게 된다. 하지만 해당 과정은 병렬적으로 계산되는 것이 아닌 각 Bounding Box에 대해서 1:1매칭으로 비교되는 계산법으 높은 시간 복잡도를 가지고 있어 최종 결과물의  Latency시간에 큰 영향을 미치게 된다.</p>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/3dc242d8-2b81-488a-9f22-b00fcb1ba538/image.png" alt="">
<img src="https://velog.velcdn.com/images/chan_woo_00/post/5063d156-d092-4ebf-b24f-baa711462923/image.png" alt="">
NMS의 단점으로는 다른 객체에도 영향을 미치게 되는 것으로 클래스별로 계산되는 과정에서 같은 클래스의 Bounding Box가 2개 이상 존재하는 경우에 서로 다른 객체이더라도 하나만 살리는 등의 문제점이 존재한다.</p>
<p>추가적으로 NMS는 end-to-end 구조를 가진 YOLO시리즈에 대해서 부정적인 영향을 미친다고 평가되어왔다.(후처리가 직접적으로 들어가기 때문)</p>
<p><a href="https://hongl.tistory.com/180">(NMS 계산법에 대한 설명글)</a>
<a href="https://www.dbpia.co.kr/pdf/pdfView.do?nodeId=NODE11514115&amp;googleIPSandBox=false&amp;mark=0&amp;minRead=5&amp;ipRange=false&amp;b2cLoginYN=false&amp;icstClss=010000&amp;isPDFSizeAllowed=true&amp;accessgl=Y&amp;language=ko_KR&amp;hasTopBanner=true">(NMS 계산에 대해 개선을 시도하려 하였던 논문)</a></p>
<p>NMS의 대체제로 DETR(End-to-End Object DEtection-with-TRansfor)이나 RT-DETR(Real-Time-DEtection-with-Transformers)등을 도입해 보았지만 정확도와 추론 오버헤드(다른 객체를 없애는 등)의 문제점이 남아있다는 단점이 존재한다.</p>
<p>Backbone과 Neck 영역에서 계산의 효율성과 정확도에 대한 기능은 충분히 이끌어 내어서 자신들은 Head영역에서 진행하였고 방식으로는 Dual Label Assignments와 Consistent Match. Metric을 채택하였다.</p>
<p>이후 연산 오버헤드와 정확도, 효율성의 개선 방식으로 다음과 같은 방식을 사용하였다.</p>
<p>Efficiency</p>
<blockquote>
</blockquote>
<p>● Lightweight classification head.</p>
<blockquote>
</blockquote>
<p>● Spatial-channel decoupled downsampling.</p>
<blockquote>
</blockquote>
<p>● Rank-guided block design.</p>
<p>Accuracy</p>
<blockquote>
</blockquote>
<p>● Large-kernel convolution.</p>
<blockquote>
</blockquote>
<p>● Partial self-attention (PSA).</p>
<p>위 방식들을 통해 YOLOv10 -N / S / M /  B /  L /  X 의 Scale을 가진 모델들을 제작하였고 다음과 같은 성능을 보였다.</p>
<p>APval에서 좌측 값은 one-to-one Head의 값을 나타내며 우측 &#39; † &#39; 가 들어간 값은 one-to-many Head의 값을 나타낸다.
<img src=https://velog.velcdn.com/images/chan_woo_00/post/0600cf78-e7cf-4367-a3a7-132960c25c20/image.png>
<img src=https://velog.velcdn.com/images/chan_woo_00/post/646cc20c-3094-4d00-9534-df43bcaaf3f3/image.png></p>
<h3 id="methodology">Methodology</h3>
<p>이제까지의 YOLO모델들은 one-to-many Head를 적용시켜 일반적으로 <a href="https://arxiv.org/pdf/2108.07755">TAL</a><a href="https://ostin.tistory.com/431">(Task-ALigned)</a>를 기반으로 생성되었으나 NMS 사후처리에 의존해야 하기 때문에 연산량이 증가되는 문제점이 있고 NMS를 사용하지 않기 위해 제작된 one-to-one Head를 사용하는 모델에 대해서도 추론 오버헤드(존재하는 객체를 인지하지 않는)를 발생시키거나 최적화 되지 않는 문제점이 남아있다.</p>
<p>따라서 해당 논문은 one-to-many Head와 one-to-one Head 두가지를 모두 사용하는 Consist Dual Assignment를 사용하여 NMS를 제거하여 연산량을 해결하고 여러 객체에 대한 정확도를 높이는 작업을 수행함.</p>
<p>◆  <code>Consist Dual Assignment</code>는 Dual Label Assignments영역과 Consistent Match. Metric영역으로 분리되어 사용된다.
<img src=https://velog.velcdn.com/images/chan_woo_00/post/3352b4f9-f4d7-4c0d-bbdc-e8c1cf99e99a/image.png></p>
<h4 id="dual-label-assignments이중-레이블-할당">Dual Label Assignments(이중 레이블 할당)</h4>
<p>one-to-one Head는 하나의 객체에 대해 하나의 예측값을 출력하여 NMS의 사후 처리가 필요하지 않은 Head이나 Supervision에 대한 정보가 충분치 않아 Accuracy와 Convergence speed를 최적화시키지 못한다.</p>
<p>one-to-many Head는 각 객체에 대해 여러가지의 예측값을 출력하는 대신 단일로 사용될 때 NMS와 같은 후처리가 필요하다.</p>
<p>YOLOv10에서는 두 Head를 모두 사용하여 모델의 학습시에는 one-to-one Head와 one-to-many Head에서 나오는 Supervision값들을 사용하여 두 개의 Head와 Backbone 및 Neck영역의 가중치들을 빠르고 정확하게 최적화하며 추론할 때에는 one-to-many Head를 사용하지 않고 one-to-one Head를 사용하여 상위 1개의 Bounding Box를 채택하여 end-to-end 형식의 YOLO 모델을 사용한다. one-to-one매칭에서 훈련 시간이 적은 <a href="https://www.youtube.com/watch?v=cQ5MsiGaDY8">Hungarian matching</a>과 동일한 성능을 달성한다고 나와있는데 Hungarian matching은 2개의 집합에 대해 최적의 매칭을 잡아 높은 이익의 결과물을 가져오는 알고리즘을 의미한다.</p>
<h4 id="consistent-matching-metric일관된-매칭-메트릭">Consistent Matching Metric(일관된 매칭 메트릭)</h4>
<img src=https://velog.velcdn.com/images/chan_woo_00/post/8c313eb0-6a0d-4eeb-9e9f-bcb99ebc1a93/image.png>
p : 분류 점수

<p>s : 예측의 앵커 포인트가 인스턴스 내 존재하는 공간의 정보(IoU값으로 계산)</p>
<p>b_hat, b :  예측값과 인스턴스의 bbox</p>
<p>α, β : 의미 예측 작업과 회귀 작업에 사용되는 하이퍼파라미터.</p>
<p>m_o2m=m(α_o2m, β_o2m) : one-to-many에 대한 Metric</p>
<p>m_o2o=m(α_o2o, β_o2o) : one-to-one에 대한 Metric</p>
<p>Consistent Matching Metric에서는 두 Head에서 나온 Supervision들을 토대로 학습을 수행하는 영역이다.</p>
<p>두 Head에서 제공되는 Supervision의 격차를 분석하여 학습을 진행하며 최초 시행시 동일한 값으로 초기화된 두 개의 Head에 대해 검사를 시작하여 동일한 예측값을 생성되는 것을 전제로 동일한 p값과 IoU값을 생성하는 경우에는 두 Head의 결과가 동일한 것으로 판단되고 학습 시 발생하는 차이는 회귀 단계가 아닌 분류에 대한 부분에서 발생한다.</p>
<p>동일한 값을 출력한 예측 값은 공유하고 예측 값이 다른 경우에는 무시되면서 해당 격차는 1-Wasserstein distance로 도출된다.</p>
<p>1-Wasserstein distance의 거리 값이 가장 작은 때의 distance는 Wasserstein distance로 지칭된다.
<a href="https://www.slideshare.net/slideshow/wasserstein-gan-i/75554346">Wasserstein distance에 대한 설명</a></p>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/cfa58268-ae5f-47bf-b6fb-bf6c5ac1bc43/image.png" alt="">
A는 Supervision의 격차를 의미하며이를 최소화하기 위해 Metric값을 𝛼𝑜2𝑜=𝑟⋅𝛼𝑜2𝑚 및 𝛽𝑜2𝑜=𝑟′⋅𝛽𝑜2𝑚으로 설정되고 이는 하단의 식을 의미한다.</p>
<img src=https://velog.velcdn.com/images/chan_woo_00/post/717d1ab4-1c28-4a07-9e0c-359ebb898e70/image.png aling='right'>




<p>위 과정을 통해 학습 시 one-to-many Head의 최상의 표본은 one-to-one Head에서도 최상의 표본이 되며 두 헤드간의 결과를 일관성을 유지하며 모델을 최적화 시킬 수 있다.</p>
<h4 id="holistic-efficiency-accuracy-driven-model-design">Holistic Efficiency-Accuracy Driven Model Design</h4>
<p>효율적으로 모델을 운용하기 위해서 YOLO의 구성 요소 중 계산 비용이 낮은 stem을 제외한 downsampling layers, basic building blocks, head에 대해 설계를 수행함.</p>
<img src=https://velog.velcdn.com/images/chan_woo_00/post/c60bd0f6-6417-48c1-9edb-2f0835f21f39/image.png>
이전 모델들의 Head를 조사하던 도중 특이점을 발견하였는데 Classification Head와 Regression Head에 있어 각 영향을 분석한 결과 Classification Head은 학습에 별로 도움이 되지 않고 Regression Head가 YOLO 성능에 더 높은 영향을 미치는 것을 확인하였다. 따라서 Classification Head를 조금 lightweight화 시켜 계산에 있어 조금 더 낮은 리소스를 투자하도록 하였다.

<p>(분류 헤드는 객체의 클래스의 확률을 정하며 회귀는 Bounding Box의 좌표와 각 예측에 대한 신뢰도를 계산)</p>
<h4 id="spatial-channel-decoupled-downsampling-공간-채널-분리-다운샘플링">Spatial-channel decoupled downsampling (공간-채널 분리 다운샘플링)</h4>
<img src=https://velog.velcdn.com/images/chan_woo_00/post/7e7114a9-edc4-4a0a-bed5-970e9dc5ab30/image.png>
이전의 모델들은 stride 2의 값을 지닌 3*3 convolution 작업에 대해서 공간적 다운샘플링 (H * W)을 (H/2 * W/2)로 나누고 채널을 (C에서 2C)로 늘림 위 작업은 O(9^2HWC^2)의 계산 비용과 O(18C^2)의 파라미터 수를 가지게 되는데
pointwise convolution을 통해서 Channel Dimension을 조절하고 이후 Depthwise Convolution작업을 수행하여 다운샘플링을 수행하였다.

<p>해당 작업은 O(2HWC2 + (9/2)HWC) 계산 비용과 O(2C2+18C)의 매개변수 수로 감소되는 결과를 도출하였다.
Pointwise는 3차원 Depth의 레이어들이 있다면 1<em>1</em>3의 fillter를 통과시켜 1차원 Depth의 레이어 하나가 있도록 하는 기법으로 채널 자원을 조정하며 Depthwise convolution은 3 차원 Depth의 레이어가 있을 경우 각 차원별로 3<em>3</em>1 filter로 conv를 진행시켜 각 Depth에 대해 합성곱을 진행한다.</p>
<p>해당 방식은 다운 샘플링을 진행하는 동안 정보의 변화를 최소화하면서도 Latency 값의 감소를 유도한다.</p>
<h4 id="rank-guided-block-design순위-기반-블록-설계">Rank-guided block design(순위 기반 블록 설계)</h4>
<p>YOLO모델들은 특징을 추출하는 단계에서 모델이 크고 깊은 수준의 특징을 추출할 때 같은 내용의 연산을 중복하여 연산하는 경우가 많다. 이를 해결하기 위해 CIB(Compact Inverted Block)를 사용한다 CIB는 바로 위에서 설명한 depthwise convolution와 pointwise convolution을 채택한 블록 구조이다.</p>
<p>CIB 블록은 <a href="https://arxiv.org/pdf/2211.04800">ELAN</a>(efficient layer aggregation network)구조에 포함되어 YOLOv10의 기본 구조로 포함된다.</p>
<p>ELAN을 간단하게 설명하자면 특성맵을 받아 여러 채널 그룹으로 분할시키고 각 그룹을 독립적으로 처리하며 서로 다른 해상도의 특징들을 조합하며 학습이 가능한 블록 구조이다. 
<img src="https://velog.velcdn.com/images/chan_woo_00/post/e6e26dea-da18-4384-bb23-5edd57401cbf/image.png" alt=""></p>
<h4 id="accuracy-driven-model-design">Accuracy driven model design</h4>
<p>모델의 정확도를 높이기 위한 모델 설계로는 Large-kernel convolution과 Partial self-attention(PSA)가 있다.
<img src=https://velog.velcdn.com/images/chan_woo_00/post/90c91f2f-9566-454e-a94c-a2535e55a11f/image.png></p>
<p>각 특성 맵은 깊이에 따라 특성간의 거리에 있어 같은 값들을 가지게 되는데 모든 단계에서 3<em>3의 kernel size를 유지하게 되면 작은 객체를 감지할 때 얕은 수준의 특성의 학습에 대해 부정적인 영향을 미치고 고해상도 단계에서는 같은 특성의 학습을 계속해서 반복하는 등의 컴퓨팅 소스를 낭비하게 된다. 해당 문제를 해결하기 위해서 깊은 단계의 특성맵들을 계산할 때에는 CIB 내부에서 Dilation rate값을 증가시키면서 kernel size를 7</em>7로 증가시키는 작업을 통해 추론-오버헤드(계산량의 증가) 없이 수용영역(kernel size가 증가함에 따른 전역적인 특징의 감지)을 늘린다.</p>
<p>(사진에서는 Kernel size가 3 * 3으로 유지되고 있지만 각각 5<em>5와 7</em>7이 맞는 표현이다. 그림에서는 추론-오버헤드 없이 진행됨을 강조하기 위해 kernel size를 3*3으로 유지한 것으로 보임.)</p>
<p>(해당 합성곱은 대형 모델에 대해서는 사용하지 않을 수 있다. - 이미 많은 파라미터와 가중치를 가지고 있기 때문이다.)</p>
<h4 id="partial-self-attentionpsa부분적-자기-주의---사진의-c에-해당">Partial Self-Attention(PSA)(부분적 자기 주의) - 사진의 C에 해당</h4>
<p><a href="https://www.youtube.com/watch?v=3W8B7ma7oFo">Attention</a>에 대한 설명(이미지로 예시를 들다면 각 특성들에 대한 연관성이고 문장으로 예시를 들면 각 단어 각 문장 사이의 연관성의 강도를 의미한다.)</p>
<img src=https://velog.velcdn.com/images/chan_woo_00/post/6abda5a5-5177-4215-9dce-46e9a9f45eb7/image.png aling=right>

<p><a href="https://codingopera.tistory.com/43">Self-attention</a>은 널리 사용되지만 높은 계산 복잡도와 메모리 사용량을 동반하기에. 1x1 convolution 후에 채널을 두 부분으로 나누어 한 부분만 <a href="https://codingopera.tistory.com/43">MHSA</a>(Multi-Head Self-Attention)와 <a href="https://en.wikipedia.org/wiki/Feedforward_neural_network">FFN</a>(Feed-forward Network) 블록을 통과하게 하고 Self-Attention 작업은 낮은 해상도를 가지는 파트에서만 적용되어 낮은 계산 비용으로 Self-Attention작업을 수행한다. 그림의 [* N_PSA]는 해당 점선으로 그려진 영역의 반복 횟수를 의미한다.</p>
<p>이후 처음 분리했던 블록과 1*1 convolution에 의해 합쳐진다.</p>
<p>논문에서는 가장 낮은 해상도인 Stage 4 이후에만 배치된다고 하나 Stage 4가 어느 지점을 이야기하는지 정확히 명칭하는 바는 아직 확인하지 못하였다.</p>
<p>해당 작업에서 빠른 추론을 위해 LayerNorm을 BatchNorm으로 변경하는 등의 작업도 수행되었다</p>
<h4 id="conclusion">Conclusion</h4>
<p>NMS 후처리를 대체하기 위해   Consist Dual Assignment을 제안</p>
<p>전체적인 모델의 정확도와 효율성을 개선하기 위해 합성곱을 진행하는 방식과 커널 사이즈 및 Self-Attention을 부분적으로 적용하는 등의 작업을 통해 연산량은 유지(Kernel Size를 증가시키며 Dilation rate값도 증가) 혹은 감소(중복하여 진행하는 연산은 제거)시키면서 정확도를 증가시킴.</p>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/b55bcd85-dd47-42e1-b72f-ff899afe7250/image.png" alt="">
<a href="https://www.youtube.com/watch?v=A6rHMzRvs98">레이어 설명</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[YOLO 모델 구조_Head(Neck)편 ]]></title>
            <link>https://velog.io/@chan_woo_00/YOLO-%EB%AA%A8%EB%8D%B8-%EA%B5%AC%EC%A1%B0HeadNeck%ED%8E%B8</link>
            <guid>https://velog.io/@chan_woo_00/YOLO-%EB%AA%A8%EB%8D%B8-%EA%B5%AC%EC%A1%B0HeadNeck%ED%8E%B8</guid>
            <pubDate>Mon, 20 Oct 2025 11:17:50 GMT</pubDate>
            <description><![CDATA[<p>Neck 하나로 통합</p>
<img src=https://velog.velcdn.com/images/chan_woo_00/post/860742ae-8d38-497a-b0e7-d3e634225c2e/image.webp>

<p>Neck와 Head 분리</p>
<img src=https://velog.velcdn.com/images/chan_woo_00/post/e9abdb49-bcd4-4de8-9536-bc591e4f0019/image.png>


<p>Neck영역 추출 이미지</p>
<img src=https://velog.velcdn.com/images/chan_woo_00/post/1a44776a-eafa-496f-a601-64d3ca085742/image.png>
Backbone 레이어의 4번, 6번, 9번 레이어에서 특성 맵(상위, 중위, 하위 단계 특성)을 받아 neck영역으로 보낸다.

<p>이후 Upsample과정에서는 nearest방식과 scale_factor값에 2를 주어 이미지의 크기를 상승시킨다.</p>
<p>사용되는 torch Upsample 코드 예시</p>
<img src=https://velog.velcdn.com/images/chan_woo_00/post/daf2d890-3220-44fe-8e0d-6eb18097cff3/image.png>

<p>각각에서 추출된 레이어들은 Upsample 레이어와 Cov 레이어를 거치며 각 이미지 사이즈의 특성을 합친 특성맵을 출력하고 이를 Head의 입력으로 사용해 각 사이즈(특성맵의 width, height)의 정보를 가진 Detect 클래스가 생성된다.</p>
<img src=https://velog.velcdn.com/images/chan_woo_00/post/b5facbf2-9924-482c-af8f-5076dbc94433/image.png>

<p>Upsample레이어에 관해 10번 레이어와 13번 레이어에 대해 출력 특성 맵의 차원이 다른 것을 확인할 수 있었는데 Upsample레이어 내부에 출력 채널 수를 결정하는 레이어가 존재하나 model.yaml에서는 해당 요소에 대하여 인가하는 파라미터를 찾을 수 없어 현재 파악 중 (c_ 파라미터가 출력 채널의 수로 256이다 이로 인해 13번의 채널 수가 줄어드는 것은 파악할 수 있었으나 10번 레이어를 통과할 때 채널 수가 줄어들이 않는 이유는 확인하지 못함)</p>
<p>각각에서 추출된 레이어들은 Upsample 레이어와 Cov 레이어를 거치며 각 이미지 사이즈의 특성을 합친 특성맵을 출력하고 이를 Head의 입력으로 사용해 각 사이즈(특성맵의 width, height)의 정보를 가진 Detect 클래스가 생성된다.</p>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/53f71066-9495-4311-b416-af8895f09e46/image.png" alt="">
블록도에서는 서로 다른 Detect에 연결되는 것처럼 보이지만 실제 코드를 살펴보면 각각의 레이어를 하나의 Head가 모두 받는것을 확인할 수 있다.</p>
<p>참고 사이트
<a href="https://docs.ultralytics.com/ko/yolov5/tutorials/architecture_description/#44-build-targets">https://docs.ultralytics.com/ko/yolov5/tutorials/architecture_description/#44-build-targets</a>
<a href="https://www.reddit.com/r/Ultralytics/comments/1eolwl8/the_correct_way_to_train_from_a_previously/?rdt=53319">https://www.reddit.com/r/Ultralytics/comments/1eolwl8/the_correct_way_to_train_from_a_previously/?rdt=53319</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[YOLO model 구조_Backbone. ]]></title>
            <link>https://velog.io/@chan_woo_00/YOLO-model-%EA%B5%AC%EC%A1%B0Backbone</link>
            <guid>https://velog.io/@chan_woo_00/YOLO-model-%EA%B5%AC%EC%A1%B0Backbone</guid>
            <pubDate>Mon, 20 Oct 2025 11:12:44 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-python"># Ultralytics YOLO 🚀, AGPL-3.0 license
# YOLOv10 object detection model. For Usage examples see https://docs.ultralytics.com/tasks/detect

# Parameters
nagent: 2 #number of total
nloc: 5 #number of locations
nact: 4 #number of actions
nc: [2,5,4] # number of classes

scales: # model compound scaling constants, i.e. &#39;model=yolov8n.yaml&#39; will call yolov8.yaml with scale &#39;n&#39;
  # [depth, width, max_channels]
  s: [0.33, 0.50, 1024]


backbone:
  # [from, repeats, module, args]
  - [-1, 1, Conv, [64, 3, 2]] # 0-P1/2
  - [-1, 1, Conv, [128, 3, 2]] # 1-P2/4
  - [-1, 3, C2f, [128, True]]
  - [-1, 1, Conv, [256, 3, 2]] # 3-P3/8
  - [-1, 6, C2f, [256, True]]
  - [-1, 1, Conv, [512, 3, 2]] # 5-P4/16
  - [-1, 6, C2f, [512, True]]
  - [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32
  - [-1, 3, C2f, [1024, True]]
  - [-1, 1, SPPF, [1024, 5]] # 9

# YOLOv8.0n head
head:
  - [-1, 1, nn.Upsample, [None, 2, &quot;nearest&quot;]]
  - [[-1, 6], 1, Concat, [1]] # cat backbone P4
  - [-1, 3, C2f, [512]] # 12

  - [-1, 1, nn.Upsample, [None, 2, &quot;nearest&quot;]]
  - [[-1, 4], 1, Concat, [1]] # cat backbone P3
  - [-1, 3, C2f, [256]] # 15 (P3/8-small)

  - [-1, 1, Conv, [256, 3, 2]]
  - [[-1, 12], 1, Concat, [1]] # cat head P4
  - [-1, 3, C2f, [512]] # 18 (P4/16-medium)

  - [-1, 1, Conv, [512, 3, 2]]
  - [[-1, 9], 1, Concat, [1]] # cat head P5
  - [-1, 3, C2f, [1024]] # 21 (P5/32-large)

  - [[15, 18, 21], 1, Multi_v10Segment, [nc, 32, 256]] # Detect(P3, P4, P5)</code></pre>
<p><code>multitask</code>에 대한 YOLO model 구조도 yaml파일</p>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/f906f0c9-9b4d-4a7e-b3e9-4f8356fc60e4/image.webp" alt="">
Backbone : 모델을 사용할 때 사전에 학습되어있는 딥러닝 모델에서 특징을 추출할 때 사용되는 기반 신경망 구조.</p>
<p>Backbone영역에 들어가는 Layer 목록</p>
<p>&lt; Conv, C2f, SPPF&gt;</p>
<p>Conv(Convolution Layer)</p>
<p>합성곱 레이어로 이미지의 특징(feature)을 추출하는 기초 레이어.</p>
<blockquote>
<p>[ k (kernel) : 커널 크기, s (stride) : 스트라이드, p (pading) = 패딩 ]</p>
</blockquote>
<p>kernel에 들어가는 값들에 대해서는 초회차에 무작위로 생성된 이후 다음 학습에 대해 최적화된다.</p>
<p>Conv Layer는 크게 Conv2d, BatchNorm2d, SiLU로 구성되어 있다.</p>
<p>Conv2d</p>
<p>합성곱을 진행하는 파트로 사진의 원본 이미지 혹은 합성곱이 진행된 특성 맵을 입력으로</p>
<p>받아 합성곱을 진행하여 출력으로 내보낸다.</p>
<p>BatchNorm2d</p>
<p>Conv2d로 진행된 특성 맵을 입력으로 받고 각 채널에 대하여 평균과 분산을 계산한 후 정규화를 진행한다.</p>
<p>이후 정규화 된 값에 특정 Scale과 이동 값을 각각 곱하고 더한 이후 특성 맵을 출력으로 내보낸다.</p>
<p>(미니배치 단위로 정규화를 진행하여 Batch + Norm으로 이름이 지어졌다. 2D는 차원(이미지에 대한.))</p>
<p>SiLu</p>
<p>활성화 함수 중 하나로 특성 맵을 입력으로 받아 활성화 값을 곱해준 이후 출력으로 특성맵을 내보낸다.</p>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/2ddfc78f-9873-4713-98cd-858d97847301/image.png" alt=""></p>
<p><a href="https://tae-jun.tistory.com/10">SiLU vs ReLU 설명하는 사이트</a></p>
<h4 id="conv">Conv</h4>
<pre><code class="language-python">class Conv(nn.Module):
    &quot;&quot;&quot;Standard convolution with args(ch_in, ch_out, kernel, stride, padding, groups, dilation, activation).&quot;&quot;&quot;

    default_act = nn.SiLU()  # default activation
    #c1 : 입력 채널, c2 : 출력 채널, k : kernel_size, s : stride, p : padding, g : group, d : dilation(필터 간격), act = bias
    #default_act는 활성화 함수
    def __init__(self, c1, c2, k=1, s=1, p=None, g=1, d=1, act=True):
        &quot;&quot;&quot;Initialize Conv layer with given arguments including activation.&quot;&quot;&quot;
        super().__init__()
        self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p, d), groups=g, dilation=d, bias=False)
        #BatchNorm2d 노드가 bn으로 저장
        self.bn = nn.BatchNorm2d(c2)
        #act == True이면 default_act 사용, 아니라면 act로 명시된 활성화함수를 사용. act에 ReLU를 사용하면 ReLu활성화 함수 사용.
        self.act = self.default_act if act is True else act if isinstance(act, nn.Module) else nn.Identity()

    def forward(self, x):
        &quot;&quot;&quot;Apply convolution, batch normalization and activation to input tensor.&quot;&quot;&quot;
        return self.act(self.bn(self.conv(x)))

    def forward_fuse(self, x):
        &quot;&quot;&quot;Perform transposed convolution of 2D data.&quot;&quot;&quot;
        return self.act(self.conv(x))</code></pre>
<h4 id="conv2">Conv2</h4>
<pre><code class="language-python">class Conv2(Conv):
    &quot;&quot;&quot;Simplified RepConv module with Conv fusing.&quot;&quot;&quot;
    #c1 : 입력 채널, c2 : 출력 채널, k : kernel_size, s : stride, p : padding, g : group, d : dilation(필터 간격), act = bias
    #Conv를 상속하여 대부분의 파라미터 값을 이어 받는다.
    def __init__(self, c1, c2, k=3, s=1, p=None, g=1, d=1, act=True):
        &quot;&quot;&quot;Initialize Conv layer with given arguments including activation.&quot;&quot;&quot;
        super().__init__(c1, c2, k, s, p, g=g, d=d, act=act)
        self.cv2 = nn.Conv2d(c1, c2, 1, s, autopad(1, p, d), groups=g, dilation=d, bias=False)  # add 1x1 conv

    def forward(self, x):
        &quot;&quot;&quot;Apply convolution, batch normalization and activation to input tensor.
            두 개의 합성곱(conv와 cv2)의 결과를 더한 이후 활성화 함수를 적용.&quot;&quot;&quot;
        return self.act(self.bn(self.conv(x) + self.cv2(x)))

    def forward_fuse(self, x):
        &quot;&quot;&quot;Apply fused convolution, batch normalization and activation to input tensor.
            병합된 단일 합성곱의 결과를 활성화 함수에 전달 -&gt;forward와 차이점 : &quot;&quot;&quot;
        return self.act(self.bn(self.conv(x)))

    def fuse_convs(self):
        &quot;&quot;&quot;Fuse parallel convolutions.
            병렬 합성곱(conv, cv)을 병합하여 최적화하고 병합 이후 forward_fuse만 수행 -&gt; 연산량 감소 효과&quot;&quot;&quot;
        w = torch.zeros_like(self.conv.weight.data)
        i = [x // 2 for x in w.shape[2:]]
        w[:, :, i[0] : i[0] + 1, i[1] : i[1] + 1] = self.cv2.weight.data.clone()
        self.conv.weight.data += w
        self.__delattr__(&quot;cv2&quot;)
        self.forward = self.forward_fuse</code></pre>
<p>해당 코드를 보면 Conv2와 Conv 클래스 내부에 활성화 함수를 적용시키는 것을 확인할 수 있는데 위 클래스를 각 파트별로 분해시킨 것이 Conv과 Conv2 블록이다. 사용자가 편하게 볼 수 있도록 분할하여 그린 것으로 추정.</p>
<h4 id="c2fcoordinates-to-features">C2f(Coordinates-To-Features)</h4>
<img src=https://velog.velcdn.com/images/chan_woo_00/post/b1b9423d-b045-4c9e-8dc4-673efbecad10/image.png alig='left'>


<p>특성맵 처리와 성능 향상 레이어.
multi-scale object detection에 대한 성능을 상향.
Conv, Split, Bottleneck, Concat 블록으로 이루어져 있다</p>
<p>Conv(Convolution Layer)
이전의 k, s, p에 이어 c가 새로 생겼는데 이는 c_out으로 출력 채널의 수를 의미.
코드의 클래스에서는 c2라는 파라미터 이름으로 들어가 있다.
(특성 맵들이 서로 다른 병)</p>
<p>Split</p>
<p>특성 맵 전체를 받아 5 : 5 비율로 특성 맵을 분리시켜주는 블록.
전체 특성 맵(차원)이 128이면 64 : 64로 특성맵을 분리시켜 출력한다.
Split이후 라에서 h * w * 0.5c_out으로 표현되는 것을 확인 할 수 있다.
C2f의 일부 레이어에서 n = 6<em>d, n = 3</em>d로 나와있는 것이 있는데
이는 후의 bottleneck의 횟수를 정하는 변수이다.</p>
<h4 id="bottlenect">Bottlenect</h4>
<img src=https://velog.velcdn.com/images/chan_woo_00/post/57920afe-f548-4ebe-9892-7c376dcbb830/image.png align="right">


<p>특성 맵 처리를 담당하는 블록.
입력 특징 맵을 압축하고 확장하는 방식을 통해 연산 결과를 병합한다.
내부 shortcut이라는 파라미터를 확인할 수 있는데 이는 Bottleneck 블록을 수행할 때 사용하는 파라미터로 shortcut이 True이면 입력 값을 따로 보관한 Conv 블록을 거친 특성맵과 Concat블록에서 합쳐지고(이어짐) shortcut이 False면 원본을 보관하지 않고 바로 Conv 블록을 거쳐 특성 맵을 출력한다.
처음 Conv 블록에서 채널 수를 축소하여 출력하는 이유는 모델의 계산에 있어 간결하게 하기 위함이며 불필요하다고 판단되는 특성들을 줄이고 중요하다고 판단되는 특징맵의 수를 늘리는 작업을 수행한다.</p>
<pre><code class="language-python">class Bottleneck(nn.Module):
    &quot;&quot;&quot;Standard bottleneck.&quot;&quot;&quot;

    def __init__(self, c1, c2, shortcut=True, g=1, k=(3, 3), e=0.5):
        &quot;&quot;&quot;Initializes a bottleneck module with given input/output channels, shortcut option, group, kernels, and
        expansion.
        &quot;&quot;&quot;
        super().__init__()
        c_ = int(c2 * e)  # hidden channels
        self.cv1 = Conv(c1, c_, k[0], 1)
        self.cv2 = Conv(c_, c2, k[1], 1, g=g)
        self.add = shortcut and c1 == c2

    def forward(self, x):
        &quot;&quot;&quot;&#39;forward()&#39; applies the YOLO FPN to input data.&quot;&quot;&quot;
        return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))</code></pre>
<p>Backbone의 4번 6번 9번 레이어를 보게 된다면 같은 출력값을 가지고 있으나 Concat으로 연결되며 Stride가 추가적으로 적힌 블록도를 확인할 수 있다.</p>
<img src=https://velog.velcdn.com/images/chan_woo_00/post/e1c9978a-4b16-41cb-9f0d-ea68a25bb1dd/image.png>

<p>해당 블록도에서 Stride는 일반적인 의미로 사용되는(필터간 간격) Stride가 아닌 다운샘플링이 진행된 총 비율로 4번 레이어에서는 총 8배의 다운샘플링이 진행되었다는 의미이다.</p>
<p>원본 이미지 640<em>640에 대해서 1/2의 다운샘플링 과정이 총 8배가 되었다는 의미로 Conv레이어를 총 3번 거치며 2</em>2*2에 대해 640 / 8 = 80이 출력되었다는 것을 확인시켜주는 보조의 의미를 가진다.</p>
<p>이후에 각각 Conv 레이어를 거치며 Stride의 값이 2<em>2</em>2*2로 6번 레이어에서는 총 16배의 다운샘플링이 진행되었고 9번 레이어에서는 32배의 다운샘플링이 진행되었다는 것을 확인할 수 있다.</p>
<p>6번 레이어 -&gt; 640 / 16 = 40, 8번 레이어 -&gt; 640 / 32 = 20 </p>
<pre><code class="language-python">from ultralytics import YOLO
import multiprocessing
from ultralytics import settings
import matplotlib.pyplot as plt


model = YOLO(&quot;yolov10s-seg.yaml&quot;)

print(&quot;4번 레이어&quot;)
print(model.model.model[4])

print(&quot;6번 레이어&quot;)
print(model.model.model[6])

print(&quot;9번 레이어&quot;)
print(model.model.model[9])

import torch

model = YOLO(&quot;yolov10s-seg.yaml&quot;)

dummy_input = torch.randn(1, 3, 640, 640)

x = dummy_input
for idx, layer in enumerate(model.model.model):
    x = layer(x)
    if idx in [4, 6, 9]:  # C2f layers
        print(f&quot;Layer {idx}: Output shape {x.shape}&quot;)</code></pre>
<img src=https://velog.velcdn.com/images/chan_woo_00/post/1de2ec58-52af-4a75-ab4a-59c238151ddc/image.png>


<h4 id="concat">Concat</h4>
<p>여러개의 레이어를 하나로 합치는 과정으로 같은 채널 크기의 특성맵들을 입력으로 받아 특성맵끼리 이어 하나의 특성 맵을 출력으로 내보낸다.</p>
<h4 id="sppfspatial-pyramid-pooling---fast">SPPF(Spatial Pyramid Pooling - Fast)</h4>
<p>Conv, MaxPool2d, Concat 블록으로 이루어진 블록.</p>
<p>여러 크기의 특성들에(작은 특성, 중간 특성, 큰 특성) 대해 정보를 결합하여 일반화 된 특성 맵을 제공.</p>
<p>MaxPool2d : Pooling작업을 수행할 때 사용하는 기법</p>
<p>YOLO에서는 AveragePooling이 아닌 MaxPooling기법을 사용함.</p>
<p>Pooling작업을 수행하면 사이즈가 달라져서 Concat 블록에서 합칠 수 없다고 생각할 수 있지만 코드 내부에서 진행할 때 padding작업을 진행해줌으로서 크기에 대한 문제를 해결한다.</p>
<img src= https://velog.velcdn.com/images/chan_woo_00/post/eec99a9d-70f4-400d-afa5-8be7dd276fb2/image.png>

<pre><code class="language-python">class SPPF(nn.Module):
    &quot;&quot;&quot;Spatial Pyramid Pooling - Fast (SPPF) layer for YOLOv5 by Glenn Jocher.&quot;&quot;&quot;

    def __init__(self, c1, c2, k=5):
        &quot;&quot;&quot;
        Initializes the SPPF layer with given input/output channels and kernel size.
        This module is equivalent to SPP(k=(5, 9, 13)).
        레이어 초기화 등등. k는 MaxPooling을 진행할 때 사용하는 커널의 크기.
        &quot;&quot;&quot;
        super().__init__()
        c_ = c1 // 2  # hidden channels
        self.cv1 = Conv(c1, c_, 1, 1)
        self.cv2 = Conv(c_ * 4, c2, 1, 1)
        self.m = nn.MaxPool2d(kernel_size=k, stride=1, padding=k // 2)

    def forward(self, x):
        &quot;&quot;&quot;Forward pass through Ghost Convolution block. 순전파&quot;&quot;&quot;
        y = [self.cv1(x)]
        y.extend(self.m(y[-1]) for _ in range(3))
        return self.cv2(torch.cat(y, 1))
        class SPPF(nn.Module):
    &quot;&quot;&quot;Spatial Pyramid Pooling - Fast (SPPF) layer for YOLOv5 by Glenn Jocher.&quot;&quot;&quot;

    def __init__(self, c1, c2, k=5):
        &quot;&quot;&quot;
        Initializes the SPPF layer with given input/output channels and kernel size.
        This module is equivalent to SPP(k=(5, 9, 13)).
        레이어 초기화 등등. k는 MaxPooling을 진행할 때 사용하는 커널의 크기.
        &quot;&quot;&quot;
        super().__init__()
        c_ = c1 // 2  # hidden channels
        self.cv1 = Conv(c1, c_, 1, 1)
        self.cv2 = Conv(c_ * 4, c2, 1, 1)
        self.m = nn.MaxPool2d(kernel_size=k, stride=1, padding=k // 2)

    def forward(self, x):
        &quot;&quot;&quot;Forward pass through Ghost Convolution block. 순전파&quot;&quot;&quot;
        y = [self.cv1(x)]
        y.extend(self.m(y[-1]) for _ in range(3))
        return self.cv2(torch.cat(y, 1))</code></pre>
<p>관련 글
<a href="https://github.com/ultralytics/ultralytics/issues/9042">https://github.com/ultralytics/ultralytics/issues/9042</a>
<a href="https://github.com/ultralytics/ultralytics/issues/15596">https://github.com/ultralytics/ultralytics/issues/15596</a>
<a href="https://blog.roboflow.com/what-is-yolov8/#anchor-free-detection">https://blog.roboflow.com/what-is-yolov8/#anchor-free-detection</a>
<a href="https://github.com/ultralytics/ultralytics/issues/3678">https://github.com/ultralytics/ultralytics/issues/3678</a>
<a href="https://github.com/ultralytics/ultralytics/issues/13441">https://github.com/ultralytics/ultralytics/issues/13441</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[YOLO loss]]></title>
            <link>https://velog.io/@chan_woo_00/YOLO-loss</link>
            <guid>https://velog.io/@chan_woo_00/YOLO-loss</guid>
            <pubDate>Mon, 20 Oct 2025 06:46:45 GMT</pubDate>
            <description><![CDATA[<p>객체 검출을 진행하는 AI 모델에는 2가지의 방법이 있다.</p>
<p>&lt;1-stage detector, 2-stage detector&gt;</p>
<p>1-stage 방식의 대표적인 모델은 YOLO 시리즈와 Retina-Net, SSD 등이 있다.</p>
<p>1-stage 방식이란 Regional Proposal과 Classification이 CNN을 통해 동시에 이루어지는 방식으로 Convolution Layer을 통해 Feature Maps가 생성되면 Output으로  Multi-Class Classification과 Bounding Box Regression을 출력한다.</p>
<p>위 방식의 구조는 Anchor Boxes(앵커 박스)를 찾게되는데 Anchor box란 중심 좌표를 기준으로 여러 크기와 비율을 가지고 생성된 영역이다.</p>
<p><a href="https://developer-lionhong.tistory.com/70">Anchor box 관련 글</a></p>
<p>돌아와 YOLO 시리즈에서 loss값을 계산하여 학습시키는 모듈은 ultralytics/nn/tasks.py 으로 해당 모듈은 기본적으로 다른 Detection, Segmentation, Poss 등의 작업에 대한 기초 클래스 역할을 하는 BaseModel이 있다.</p>
<p>class BaseModel 은 모델의 기초로서 YOLOv8의 공통적인 레이어 구성과 파라미터 초기화를 초기화하여 이후에 이를 상속받는 모델들에 있어 기초 토대를 만들어준다. </p>
<p>기초 토대를 구성해 준다는 것은 손실 값 계산, 모델의 가중치 로드, 순방향 패스의 수행에 있어 입력값과 출력값 그 사이에 필요한 파라미터(특성 맵을 저장할지에 대한 여부)등 이 있다.</p>
<pre><code class="language-python">class DetectionModel(BaseModel)
        # Define model
        ch = self.yaml[&quot;ch&quot;] = self.yaml.get(&quot;ch&quot;, ch)  # input channels
        if nc and nc != self.yaml[&quot;nc&quot;]:
            LOGGER.info(f&quot;Overriding model.yaml nc={self.yaml[&#39;nc&#39;]} with nc={nc}&quot;)
            self.yaml[&quot;nc&quot;] = nc  # override YAML value
        self.model, self.save = parse_model(deepcopy(self.yaml), ch=ch, verbose=verbose)  # model, savelist
        self.names = {i: f&quot;{i}&quot; for i in range(self.yaml[&quot;nc&quot;])}  # default names dict
        self.inplace = self.yaml.get(&quot;inplace&quot;, True)
        self.end2end = getattr(self.model[-1], &quot;end2end&quot;, False)</code></pre>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/9552ee4f-68b7-49d9-ab0c-94de22b0e0f5/image.png" alt=""></p>
<pre><code class="language-python">def loss(self, batch, preds=None):
        &quot;&quot;&quot;
        Compute the loss for the given batch of data.
        Args:
            batch (dict): Dictionary containing image and label data.
            preds (torch.Tensor, optional): Precomputed model predictions. Defaults to None.
        Returns:
            (tuple): A tuple containing the total loss and main three losses in a tensor.
        &quot;&quot;&quot;
        if not hasattr(self, &quot;criterion&quot;):
            self.criterion = self.init_criterion()

        img = batch[&quot;img&quot;]
        # NOTE: preprocess gt_bbox and gt_labels to list.
        bs = len(img)
        batch_idx = batch[&quot;batch_idx&quot;]
        gt_groups = [(batch_idx == i).sum().item() for i in range(bs)]
        targets = {
            &quot;cls&quot;: batch[&quot;cls&quot;].to(img.device, dtype=torch.long).view(-1),
            &quot;loc&quot;: batch[&quot;loc&quot;].to(img.device, dtype=torch.long).view(-1),
            &quot;action&quot;: batch[&quot;action&quot;].to(img.device, dtype=torch.long).view(-1),
            &quot;bboxes&quot;: batch[&quot;bboxes&quot;].to(device=img.device),
            &quot;batch_idx&quot;: batch_idx.to(img.device, dtype=torch.long).view(-1),
            &quot;gt_groups&quot;: gt_groups,
        }

        preds = self.predict(img, batch=targets) if preds is None else preds
        dec_bboxes, dec_scores, enc_bboxes, enc_scores, dn_meta = preds if self.training else preds[1]
        if dn_meta is None:
            dn_bboxes, dn_scores = None, None
        else:
            dn_bboxes, dec_bboxes = torch.split(dec_bboxes, dn_meta[&quot;dn_num_split&quot;], dim=2)
            dn_scores, dec_scores = torch.split(dec_scores, dn_meta[&quot;dn_num_split&quot;], dim=2)

        dec_bboxes = torch.cat([enc_bboxes.unsqueeze(0), dec_bboxes])  # (7, bs, 300, 4)
        dec_scores = torch.cat([enc_scores.unsqueeze(0), dec_scores])

        loss = self.criterion(
            (dec_bboxes, dec_scores), targets, dn_bboxes=dn_bboxes, dn_scores=dn_scores, dn_meta=dn_meta
        )
        # NOTE: There are like 12 losses in RTDETR, backward with all losses but only show the main three losses.
        return sum(loss.values()), torch.as_tensor(
            [loss[k].detach() for k in [&quot;loss_giou&quot;, &quot;loss_class&quot;, &quot;loss_bbox&quot;]], device=img.device
        )</code></pre>
<p><a href="https://kimjy99.github.io/%EB%85%BC%EB%AC%B8%EB%A6%AC%EB%B7%B0/rt-detr/">참고할 사이트</a></p>
<p>ultralytics/models/yolo/detect/train.py, val.py, predict.py</p>
<p>해당 모듈에서는 모델이 학습할 때 쓰이는 loss값이 어떻게 설정되고 계산하는지 포함.</p>
<p>손실 항목은 self.loss_names에 저장.</p>
<blockquote>
<p>self.loss_names = &quot;box_loss&quot;, &quot;cls_loss&quot;, &quot;dfl_loss&quot; </p>
</blockquote>
<p><a href="https://github.com/kCW-tb/complex_detection/blob/main/ultralytics/models/yolo/detect/train.py">https://github.com/kCW-tb/complex_detection/blob/main/ultralytics/models/yolo/detect/train.py</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[YOLO, Data Augment, Dataset]]></title>
            <link>https://velog.io/@chan_woo_00/YOLO-Data-Augment-Dataset</link>
            <guid>https://velog.io/@chan_woo_00/YOLO-Data-Augment-Dataset</guid>
            <pubDate>Mon, 20 Oct 2025 06:27:47 GMT</pubDate>
            <description><![CDATA[<p>YOLOv8 코드를 기반으로 작성</p>
<p><a href="https://docs.ultralytics.com/ko/datasets/detect/#ultralytics-yolo-format">YOLO dataset document</a></p>
<h3 id="datasetyaml">DATASET.YAML</h3>
<p>YOLO model에서 데이터셋 호출을 위해서 설정하는 파일</p>
<p>위치 : ultralytics/ultralytics/cfg/dataset/custum.yaml
데이터셋이 포함되어있는 전체 데이터셋 폴더 위치, path와 내부 학습, 검증, 테스트 데이터셋의 세부 경로를 포함.</p>
<pre><code>ultralytics의 coco8.yaml.
# Train/val/test sets
# 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
path: ../datasets/coco8 # dataset root dir
train: images/train # train images
val: images/val # val images
test: # test images</code></pre><p>경로를 지정하는 방법으로는 3가지가 존재한다.</p>
<p>(경로를 지정할 떄 lables가 기준이 아닌 images가 기준이 된다.)</p>
<p>1) 경로 : 데이터셋이 들어가 있는 디렉토리의 경로를 지정. (절대경로와 상대경로 모두 가능하다.)</p>
<p>2) 파일 : 이미지의 경로를 나열한 파일을 지정하는 것으로 train의 경우 ../dataset/train/img1.jpg, ../dataset/train/img2.jpg처럼 하나의 txt파일 내부에 각 이미지의 경로를 나열하는 방식으로 저장 후 txt의 경로를 지정한다.</p>
<p>3) 리스트 : 이미지 하나하나의 경로를 직접 적어주는 방식 [&quot;/dataset/train/img1.jpg&quot;, &quot;dataset/train/img2.jpg&quot;]를 train:이후 적어주어 이미지 각각을 선택한다.</p>
<p>GlobalWheat2020.yaml 파일이 동일 dataset경로에 있는데 해당 데이터셋은 아래와 같이 하나의 dataset폴더에 모든 데이터셋을 넣고 내부 폴더로 분할시켜주었다.</p>
<pre><code>path: ../datasets/GlobalWheat2020 # dataset root dir
train: # train images
  - images/arvalis_1
  - images/arvalis_2
  - images/arvalis_3
  - images/ethz_1
  - images/rres_1
  - images/inrae_1
  - images/usask_1
val: # val images
  - images/ethz_1
test: # test images
  - images/utokyo_1
  - images/utokyo_2
  - images/nau_1
  - images/uq_1</code></pre><p>해당 방식은 리스트를 이용한 방식으로 yaml형식의 파일은 &#39; : &#39; 이 변수 명 이후에 적혀있다면 이후 &#39; - &#39;를 통해 리스트를 생성할 수 있다.</p>
<p>train을 python형식으로 고친다면 train[&quot;images/arvalis_1&quot;, &quot;images/arvalis_2&quot;, &quot;images/arvalis_3&quot;] 과 동일하게 볼 수 있다.</p>
<h4 id="데이터셋-클래스-설정">데이터셋 클래스 설정</h4>
<p>데이터셋의 경로 설정 이후 해당 데이터셋에 대한 클래스를 지정해줘야 한다.</p>
<p>&#39;names&#39;의 변수 명을 가진 리스트 형태로 작성되며 일반적으로는 해당 클래스 선언 이후 yaml 파일이 끝이난다.</p>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/c9225dac-5d35-48b6-89b5-cb59c4fc98ac/image.png" alt=""></p>
<p>eypoint는 Classes의 일부로 객체 탐지에서 &quot;호랑이&quot;로 추측되는 객체가 존재한다면 해당 객체에 대해 신체 부위 포인트를 지정하기 위해서 Keypoint가 추가적으로 사용되는 데이터이다.</p>
<h4 id="데이터셋-다운로드">데이터셋 다운로드</h4>
<p>대부분의 유저들은 처음 공부하거나 기존 코드를 사용하려 할 때 데이터셋을 미리 구현해두고 코드를 다운받지 않고 코드를 다운로드 받고 이에 해당하는 데이터셋을 찾기 때문에 대중적이거나 공개된 데이터셋의 경우 yaml파일을 읽어들이면서 존재하지 않는다면 파일을 다운로드 받을 수 있게 한다.</p>
<p>내부에 형식이 잘 정리되어있는 경우에는 아래와 같이 링크 하나만 존재하기도 하며</p>
<p>{ data8.yaml }</p>
<p>내부에 형식이 추가적으로 필요한 데이터셋의 경우 추가적인 작업을 진행해주기도 한다</p>
<pre><code># Download script/URL (optional)
download: https://github.com/ultralytics/assets/releases/download/v0.0.0/dota8.zip```

```# Download script/URL (optional) ------------------------------------------------------------------
download: |
  from ultralytics.utils.downloads import download
  from pathlib import Path

  # Download
  dir = Path(yaml[&#39;path&#39;])  # dataset root dir
  urls = [&#39;https://zenodo.org/record/4298502/files/global-wheat-codalab-official.zip&#39;,
          &#39;https://github.com/ultralytics/assets/releases/download/v0.0.0/GlobalWheat2020_labels.zip&#39;]
  download(urls, dir=dir)

  # Make Directories
  for p in &#39;annotations&#39;, &#39;images&#39;, &#39;labels&#39;:
      (dir / p).mkdir(parents=True, exist_ok=True)

  # Move
  for p in &#39;arvalis_1&#39;, &#39;arvalis_2&#39;, &#39;arvalis_3&#39;, &#39;ethz_1&#39;, &#39;rres_1&#39;, &#39;inrae_1&#39;, &#39;usask_1&#39;, \
           &#39;utokyo_1&#39;, &#39;utokyo_2&#39;, &#39;nau_1&#39;, &#39;uq_1&#39;:
      (dir / &#39;global-wheat-codalab-official&#39; / p).rename(dir / &#39;images&#39; / p)  # move to /images
      f = (dir / &#39;global-wheat-codalab-official&#39; / p).with_suffix(&#39;.json&#39;)  # json file
      if f.exists():
          f.rename((dir / &#39;annotations&#39; / p).with_suffix(&#39;.json&#39;))  # move to /annotations</code></pre><h3 id="dataloader">DATALOADER</h3>
<p>DATALOADER-dataset.py</p>
<p>YOLO는 데이터셋 호출시 공통적으로 class YOLODataset을 사용한다.</p>
<p>해당 관련 코드는 ultralytics/ultralytics/cfg/data/dataset.py에 있다.</p>
<p>생성자에 대한 초기화는 다음과 같이 이루어진다.</p>
<pre><code class="language-python">def __init__(self, *args, data=None, task=&quot;detect&quot;, **kwargs):
        self.use_segments = task == &quot;segment&quot;
        self.use_keypoints = task == &quot;pose&quot;
        self.use_obb = task == &quot;obb&quot;
        self.data = data
        assert not (self.use_segments and self.use_keypoints), &quot;Can not use both segments and keypoints.&quot;
        super().__init__(*args, **kwargs)
</code></pre>
<p>head의 속성에 따라 &#39;segment&#39;, &#39;keypoint&#39;, &#39;obb&#39;에 대한 속성을 파악하고 data는 자체적으로 받아들인다.</p>
<p>head의 속성을 입력하지 않는다면 &#39;detect&#39;으로 자동 설정된다.</p>
<p>다음 cache_lables 함수는 데이터셋 중 labeles 정보를 읽을 때 쓰이는 함수로 txt파일을 인덱스 형식으로 처리하여 정보를 읽는 방식을 설정한다.</p>
<table>
<thead>
<tr>
<th>일반</th>
<th>multitask(A-YOLOM)</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/chan_woo_00/post/627d411b-46ee-4647-9db6-4fa0af336391/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/chan_woo_00/post/57a1e3e3-097e-4cc0-8bcb-1adb72165626/image.png" alt=""></td>
</tr>
</tbody></table>
<p>우측의 그림은 detection과 segmentation작업을 모두 수행하는 A-YOLOM코드에 대한 정보이다.</p>
<h4 id="dataloader-augmentpy">DATALOADER-augment.py</h4>
<p>(프로젝트 코드로 진행 - loc와 action만 제거하면 원본 코드와 거의 동일하다, 다른 부분은 체크하고 넘길 예정.)</p>
<p>데이터 증식 관련된 함수들과 클래스가 모여있는 장소로 mosaic와 mixup 데이터 증식 기법부터 포함하여 픽셀을 건드리는 수준의 데이터 증식 기법과 공간적 측면을 건드리는 데이터 증식 기법을 가지고 있다.</p>
<p>데이터 증식에 대해서 직접적으로 건드리는 클래스는 class Albumentations이고 이 클래스가 가지고 있는 데이터 증식 기법들은 아래 하단의 사이트에 정리되어있다.
<a href="https://albumentations.ai/docs/getting_started/transforms_and_targets/#spatial-level-transforms">Albumentations Documentation</a></p>
<p>Albumentations는 공간적 속성을 변경하는 spatial_transforms과 pixel에 대해 값 똑은 색상 등을 변환하는 데이터 증식 기법들에 대해 Compose 클래스에 정리한다.</p>
<p>spatial_transforms에 나열되어있는 집합의 원소들은 사용 가능한 데이터 증식 기법들을 나타낸것이다.</p>
<pre><code class="language-python">def __init__(self, p=1.0):
        &quot;&quot;&quot;Initialize the transform object for YOLO bbox formatted params.&quot;&quot;&quot;
        self.p = p
        self.transform = None
        prefix = colorstr(&quot;albumentations: &quot;)

        try:
            import albumentations as A

            check_version(A.__version__, &quot;1.0.3&quot;, hard=True)  # version requirement

            # List of possible spatial transforms
            spatial_transforms = {
                &quot;Affine&quot;,
                &quot;BBoxSafeRandomCrop&quot;,
                &quot;CenterCrop&quot;,
                &quot;CoarseDropout&quot;,
                &quot;Crop&quot;,
                &quot;CropAndPad&quot;,
                &quot;CropNonEmptyMaskIfExists&quot;,
                &quot;D4&quot;,
                &quot;ElasticTransform&quot;,
                &quot;Flip&quot;,
                &quot;GridDistortion&quot;,
                &quot;GridDropout&quot;,
                &quot;HorizontalFlip&quot;,
                &quot;Lambda&quot;,
                &quot;LongestMaxSize&quot;,
                &quot;MaskDropout&quot;,
                &quot;MixUp&quot;,
                &quot;Morphological&quot;,
                &quot;NoOp&quot;,
                &quot;OpticalDistortion&quot;,
                &quot;PadIfNeeded&quot;,
                &quot;Perspective&quot;,
                &quot;PiecewiseAffine&quot;,
                &quot;PixelDropout&quot;,
                &quot;RandomCrop&quot;,
                &quot;RandomCropFromBorders&quot;,
                &quot;RandomGridShuffle&quot;,
                &quot;RandomResizedCrop&quot;,
                &quot;RandomRotate90&quot;,
                &quot;RandomScale&quot;,
                &quot;RandomSizedBBoxSafeCrop&quot;,
                &quot;RandomSizedCrop&quot;,
                &quot;Resize&quot;,
                &quot;Rotate&quot;,
                &quot;SafeRotate&quot;,
                &quot;ShiftScaleRotate&quot;,
                &quot;SmallestMaxSize&quot;,
                &quot;Transpose&quot;,
                &quot;VerticalFlip&quot;,
                &quot;XYMasking&quot;,
            }  # from https://albumentations.ai/docs/getting_started/transforms_and_targets/#spatial-level-transforms

            # Transforms
            T = [
                A.Blur(p=0.01),
                A.MedianBlur(p=0.01),
                A.ToGray(p=0.01),
                A.CLAHE(p=0.01),
                A.RandomBrightnessContrast(p=0.0),
                A.RandomGamma(p=0.0),
                A.ImageCompression(quality_lower=75, p=0.0),
            ]

            # Compose transforms
            self.contains_spatial = any(transform.__class__.__name__ in spatial_transforms for transform in T)
            self.transform = (
                A.Compose(T, bbox_params=A.BboxParams(format=&quot;yolo&quot;, label_fields=[&quot;class_labels&quot;]))
                if self.contains_spatial
                else A.Compose(T)
            )
            LOGGER.info(prefix + &quot;, &quot;.join(f&quot;{x}&quot;.replace(&quot;always_apply=False, &quot;, &quot;&quot;) for x in T if x.p))
        except ImportError:  # package not installed, skip
            pass
        except Exception as e:
            LOGGER.info(f&quot;{prefix}{e}&quot;)</code></pre>
<p>사용자는  T로 설정되어 있는 리스트에 원하는 증식 기법을 설정해줄 수 있다.</p>
<pre><code class="language-python"># Transforms
T = [
    A.Blur(p=0.01),
    A.MedianBlur(p=0.01),
    A.ToGray(p=0.01),
    A.CLAHE(p=0.01),
    A.RandomBrightnessContrast(p=0.0),
    A.RandomGamma(p=0.0),
    A.ImageCompression(quality_lower=75, p=0.0),
]</code></pre>
<p>이후 해당 클래스에서 Compose내역을 확인하게 되는데 Compose 즉 기본 데이터 증식(변환)을 제외하고 사용자가 설정하는 T 리스트를 읽어 적용되는 증식 기법을 확인하게 된다.</p>
<pre><code class="language-python"># Compose transforms
self.contains_spatial = any(transform.__class__.__name__ in spatial_transforms for transform in T)
self.transform = (
    A.Compose(T, bbox_params=A.BboxParams(format=&quot;yolo&quot;, label_fields=[&quot;class_labels&quot;]))
    if self.contains_spatial
    else A.Compose(T)
)</code></pre>
<p>self.contains_spatial은 any내부의 for in문을 통해 T의 리스트중 spatial_transfoms와 동일한 이름을 가진 데이터 증식 기법이 있는지 확인하게 되고 존재한다면 contains_spatial값을 True로 반환하고 존재하지 않다면 False를 반환한다.</p>
<p>이후 self.transform에서 공간적 변환이 포함된 경우에는 BboxParams를 통해 labels 데이터셋에 있는 segmentation 픽셀의 값이나 Rounding Box의 위치 정보를 이미지와 동일하게 변환해준다. (공간적 증식 이후 픽셀값관련 데이터 증식 진행.)</p>
<p>이후 호출문으로 Compose내역에 따라 데이터 증식을 적용시킨 이후 labels 데이터를 반환한다.</p>
<pre><code class="language-python">def __call__(self, labels):
        &quot;&quot;&quot;Generates object detections and returns a dictionary with detection results.&quot;&quot;&quot;
        if self.transform is None or random.random() &gt; self.p:
            return labels

        if self.contains_spatial:
            cls = labels[&quot;cls&quot;]
            loc = labels[&quot;loc&quot;]
            action = labels[&quot;action&quot;]
            if len(cls):
                im = labels[&quot;img&quot;]
                labels[&quot;instances&quot;].convert_bbox(&quot;xywh&quot;)
                labels[&quot;instances&quot;].normalize(*im.shape[:2][::-1])
                bboxes = labels[&quot;instances&quot;].bboxes
                concatenated_labels = np.concatenate([cls[:, None], loc[:, None], action[:, None]], axis=1)

                # TODO: add supports of segments and keypoints
                new = self.transform(image=im, bboxes=bboxes, class_labels=concatenated_labels)  # transformed
                if len(new[&quot;class_labels&quot;]) &gt; 0:  # skip update if no bbox in new im
                    labels[&quot;img&quot;] = new[&quot;image&quot;]
                    transformed_labels = np.array(new[&quot;class_labels&quot;])
                    labels[&quot;cls&quot;] = transformed_labels[:, 0]
                    labels[&quot;loc&quot;] = transformed_labels[:, 1]
                    labels[&quot;action&quot;] = transformed_labels[:, 2:]

                    bboxes = np.array(new[&quot;bboxes&quot;], dtype=np.float32)
                labels[&quot;instances&quot;].update(bboxes=bboxes)
        else:
            labels[&quot;img&quot;] = self.transform(image=labels[&quot;img&quot;])[&quot;image&quot;]  # transformed

        return labels</code></pre>
<p>인덱스 영역에 대해서 이야기를 하자면 넘파이 형식으로 되어있는 2차원 행렬이 있는 상태를 기반으로 했을 때 다음과 같은 형태를 가지고 있다. transformed_lables가 2차원 행렬 형태이며 {0, 1, 2 : }는 각각에 대한 정보를 가지고 있는 형태.</p>
<table>
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/chan_woo_00/post/5dea3038-726f-4ead-951d-bb411d67b7a1/image.jpg" alt=""></td>
<td><img src="https://velog.velcdn.com/images/chan_woo_00/post/aaaf5797-fa93-4c21-864a-dad2586eca22/image.png" alt=""></td>
</tr>
</tbody></table>
<p>객체검출 외 분할작업과 추가적으로 위치와 후미등 상태 여부도 파악하는 action의 경우 transformed_labels에 넘파이 형태로 넣은 다음 인덱스 형태로 적용시켰으나 원본 코드에서는 Rounding Box정보와 클래스의 정보만 있기에  바로 적용시킨 것을 확인할 수 있다.</p>
<pre><code class="language-python">if self.transform is None or random.random() &gt; self.p:
    return labels

if self.contains_spatial:
    cls = labels[&quot;cls&quot;]
    if len(cls):
        im = labels[&quot;img&quot;]
        labels[&quot;instances&quot;].convert_bbox(&quot;xywh&quot;)
        labels[&quot;instances&quot;].normalize(*im.shape[:2][::-1])
        bboxes = labels[&quot;instances&quot;].bboxes
        # TODO: add supports of segments and keypoints
        new = self.transform(image=im, bboxes=bboxes, class_labels=cls)  # transformed
        if len(new[&quot;class_labels&quot;]) &gt; 0:  # skip update if no bbox in new im
            labels[&quot;img&quot;] = new[&quot;image&quot;]
            labels[&quot;cls&quot;] = np.array(new[&quot;class_labels&quot;])
            bboxes = np.array(new[&quot;bboxes&quot;], dtype=np.float32)
        labels[&quot;instances&quot;].update(bboxes=bboxes)
else:
    labels[&quot;img&quot;] = self.transform(image=labels[&quot;img&quot;])[&quot;image&quot;]  # transformed

return labels</code></pre>
<p>위처럼 구현된 Albumentation 클래스는 같은 모듈의 v8_transforms 클래스를 통해 Mosaic, CopyPaste, RandomPerspective, 등과 함께 Compose 로 구성되어 반환되고 v8_transforms 클래스는 dataset.py의 YOLODataset클래스 내부의 build_transforms 함수에서 선언되어 구현된다.</p>
<pre><code class="language-python">def build_transforms(self, hyp=None):
    &quot;&quot;&quot;Builds and appends transforms to the list.&quot;&quot;&quot;
    if self.augment:
        hyp.mosaic = hyp.mosaic if self.augment and not self.rect else 0.0
        hyp.mixup = hyp.mixup if self.augment and not self.rect else 0.0
        transforms = v8_transforms(self, self.imgsz, hyp)
    else:
        transforms = Compose([LetterBox(new_shape=(self.imgsz, self.imgsz), scaleup=False)])
    transforms.append(
        Format(
            bbox_format=&quot;xywh&quot;,
            normalize=True,
            return_mask=self.use_segments,
            return_keypoint=self.use_keypoints,
            return_obb=self.use_obb,
            batch_idx=True,
            mask_ratio=hyp.mask_ratio,
            mask_overlap=hyp.overlap_mask,
            bgr=hyp.bgr if self.augment else 0.0,  # only affect training.
        )
    )
    return transforms</code></pre>
<h3 id="yaml">yaml</h3>
<p>yaml은 YAML Ain&#39;t Markup Language로 마크업 단어가 아니라는 정직한 이름을 가지고 있다.</p>
<p>yaml은 기존의 xml이나 json처럼 역할은 같지만 보다 사용자 친화적임에 중점을 두고 있다.</p>
<p>기존의 xml이나 json은 중괄호 대괄호 등에 각각의 역할이 주어지는 반면 yaml은 들여쓰기 하나로 해결해주어 구조 파악 등에 도움을 주고 주석 추가 여부도 다양한 사람이 입문하기 좋은 역할을 하여 YOLO에서 쓰이는 것으로 추정된다.</p>
<p>json같은 경우 labels의 역할도 이미지의 경로와 함께 저장하여 특정 YOLO모델에서 쓰이나 yaml을 사용하는 모델의 대부분인 것은 편의성과 기존 라이브러리 호출에 있어 용이하기 때문에 yaml을 주로 사용한다.</p>
<p>데이터셋의 입력 사이즈의 경우  ultralytics/cfg/default.py에서 설정할수 있다.(명령행인자로 직접 줄 수도 있다.)</p>
<pre><code># Ultralytics YOLO 🚀, AGPL-3.0 license
# Default training settings and hyperparameters for medium-augmentation COCO training

task: segment # (str) YOLO task, i.e. detect, segment, classify, pose
mode: train # (str) YOLO mode, i.e. train, val, predict, export, track, benchmark

# Train settings -------------------------------------------------------------------------------------------------------
model: # (str, optional) path to model file, i.e. yolov8n.pt, yolov8n.yaml
data: # (str, optional) path to data file, i.e. coco8.yaml
epochs: 100 # (int) number of epochs to train for
time: # (float, optional) number of hours to train for, overrides epochs if supplied
patience: 100 # (int) epochs to wait for no observable improvement for early stopping of training
batch: 32 # (int) number of images per batch (-1 for AutoBatch)
imgsz: 640 # (int | list) input images size as int for train and val modes, or list[w,h] for predict and export modes</code></pre><p>해당 모듈을 들여다보면 imgsz라는 변수가 존재하는데 해당 자료형에는 하나의 int형 변수와 list를 넣을 수 있다.</p>
<p>int형 변수를 넣게 된다면 해당 설정한 값이 가장 큰 치수를 기준으로 기존의 이미지의 크기를 변경하여 데이터셋을 구성하고 list 자료형을 넣게 된다면 해당 리스트에 맞게 규격을 맞춰준다.</p>
<p>예시를 들어 원본 1280<em>720에 대해 imgsz를 640으로 넣는다면 데이터셋은 640</em>360으로 이미지를 전처리하여 구성한다.</p>
<p>imgsz를 리스트 형태로 [640*480]으로 구성한다면 원본 이미지가 해당 규격(w,h)에 맞게 설정된다.</p>
<p>만일 이미지의 크기를 정사각형 형태로 640x640으로 구성하고 싶다면 리스트를 [640x640]으로 입력하여도 가능하지만 또 다른 변수 rect를 False에서 True로 변환시켜주는 방법도 있다 해당 변수가 True라면 640으로 입력하여도 정사각형 형태로 변환한다.</p>
<p><code>rect: False # (bool) rectangular training if mode=&#39;train&#39; or rectangular validation if mode=&#39;val</code></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[num_workers, GPU]]></title>
            <link>https://velog.io/@chan_woo_00/numworkers-GPU</link>
            <guid>https://velog.io/@chan_woo_00/numworkers-GPU</guid>
            <pubDate>Mon, 20 Oct 2025 05:11:28 GMT</pubDate>
            <description><![CDATA[<p>YOLO에서 학습을 진행하던 도중 GPU의 성능을 30%정도만 사용하는 것을 확인하였다.</p>
<p>windows환경에서 학습을 돌리며  <code>multiprocessing</code>관련 문제가 생겨 num_workers의 값을 0으로 했던 것이 문제였으며 <code>multi-processing.freeze_support()</code>함수를 추가해줌으로 해결</p>
<pre><code class="language-python">from ultralytics import YOLO
import multiprocessing

if __name__==&quot;__main__&quot;:
    multiprocessing.freeze_support()

    model = YOLO(&quot;yolov10s-seg.yaml&quot;)
    # 배치 크기 8 
    # multiprocession.freeze_support()을 쓰지 않는다면 num_worker 0 고정.
    results = model.train(data=&quot;Compete_segment.yaml&quot;,pretrained=&#39;yolov8s-seg.pt&#39;,epochs=100, device=[0], workers=4, batch=8)
</code></pre>
<p><code>num_workers</code>은 학습과정에서 데이터를 불러오는 역할을 수행할 때 사용하는 파라미터로 GPU의 연산 속도를 활용할 수 있도록 CPU의 데이터 전송 속도를 높이는 것이다.</p>
<p>num_workers의 수가 낮다면 GPU에 전송되는 데이터가 늦어 정상적으로 운용을 할 수 없으며 이 경우를 CPU I/O bottleneck현상이라고 하며 num_workers의 수를 높이게 되면 해결된다.</p>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/52eba63d-00e5-4ef6-bcd4-2f20693a486a/image.png" alt="">
1 Epoch를 진행할 때 위 사진처럼 CPU가 데이터를 GPU에 전송해주는 과정이 존재.</p>
<p>num_workers의 값을 낮게 설정하면 붉은색 선과 같이 시간이 오래 걸려 사이간 GPU를 사용하지 않게 되고
num_workers의 값을 적절히 설정하면 보라색 선과 같이 공백의 시간이 적어 GPU를 온전히 사용할 수 있게 된다.
<img src="https://velog.velcdn.com/images/chan_woo_00/post/3a6f37eb-1a6c-4c99-bbed-aae6f4409e7a/image.png" alt="">
<a href="https://jybaek.tistory.com/799">CPU와 GPU에서 num_workers에 대한 설명 글</a></p>
<p><a href="https://velog.io/@claude_ssim/NVIDIA-GPU-%EB%B3%B4%EB%8A%94%EB%B2%95nvidia-smi">nvidia-smi</a></p>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/50c11d78-0f8c-41e8-b37c-cc060dc01443/image.png" alt="">
학습을 수행할 때에는 붉은색 박스 내의 정보처럼 1 Epoch를 제외하고선 학습에 5~6분가량을 사용하고 평가지표에 1분정도를 소요함을 알 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DDRNet 가중치 조절 학습 결과]]></title>
            <link>https://velog.io/@chan_woo_00/DDRNet-%EA%B0%80%EC%A4%91%EC%B9%98-%EC%A1%B0%EC%A0%88-%ED%95%99%EC%8A%B5-%EA%B2%B0%EA%B3%BC</link>
            <guid>https://velog.io/@chan_woo_00/DDRNet-%EA%B0%80%EC%A4%91%EC%B9%98-%EC%A1%B0%EC%A0%88-%ED%95%99%EC%8A%B5-%EA%B2%B0%EA%B3%BC</guid>
            <pubDate>Mon, 13 Oct 2025 04:49:20 GMT</pubDate>
            <description><![CDATA[<h2 id="ddrnet-가중치-조절">DDRNet 가중치 조절</h2>
<h3 id="환경-구성">환경 구성.</h3>
<p>중요 환경에 대한 버전.</p>
<pre><code>Package             Version
------------------- --------------------
numpy               1.24.1
opencv-python       4.12.0.88
thop                0.1.1.post2209072238
torch               2.3.1+cu118
torchaudio          2.3.1+cu118
torchvision         0.18.1+cu118</code></pre><h3 id="진행-작업">진행 작업</h3>
<blockquote>
<ol>
<li>폴더별 가중치만 적용하여 학습.</li>
<li>클래스별 가중치만 적용하여 학습.</li>
</ol>
</blockquote>
<pre><code class="language-python">class SegmentationTransform:
    def __init__(self, crop_size=[1024, 1024], scale_range=[0.5, 1.5]):
        self.crop_size = crop_size
        self.scale_range = scale_range
        self.mean = [0.485, 0.456, 0.406]
        self.std = [0.229, 0.224, 0.225]
        self.bilinear = transforms.InterpolationMode.BILINEAR
        self.nearest = transforms.InterpolationMode.NEAREST

        # Color Jitter
        self.color_jitter = transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.3, hue=0.1)
        # Gaussian Blur
        self.gaussian_blur = transforms.GaussianBlur(kernel_size=(5, 9), sigma=(0.1, 5))

    def __call__(self, image, label):
        scale_factor = random.uniform(self.scale_range[0], self.scale_range[1])
        width, height = image.size
        new_width, new_height = int(width * scale_factor), int(height * scale_factor)
        image = TF.resize(image, (new_height, new_width), interpolation=self.bilinear)
        label = TF.resize(label, (new_height, new_width), interpolation=self.nearest)

        pad_h = max(self.crop_size[0] - new_height, 0)
        pad_w = max(self.crop_size[1] - new_width, 0)
        if pad_h &gt; 0 or pad_w &gt; 0:
            padding = (0, 0, pad_w, pad_h)
            image = TF.pad(image, padding, fill=0)
            label = TF.pad(label, padding, fill=255)

        # 크롭
        i, j, h, w = transforms.RandomCrop.get_params(image, output_size=self.crop_size)
        image = TF.crop(image, i, j, h, w)
        label = TF.crop(label, i, j, h, w)

        # 좌우 반전
        if random.random() &gt; 0.3:
            image = TF.hflip(image)
            label = TF.hflip(label)

        # 회전
        if random.random() &gt; 0.5:
            angle = random.uniform(-5, 5)
            image = TF.rotate(image, angle, interpolation=self.bilinear, fill=0)
            label = TF.rotate(label, angle, interpolation=self.nearest, fill=255)

        # 색상 변환
        if random.random() &gt; 0.4:
            image = self.color_jitter(image)
        if random.random() &gt; 0.3:
            image = self.gaussian_blur(image)

        image = TF.to_tensor(image)
        image = TF.normalize(image, mean=self.mean, std=self.std)
        label = torch.from_numpy(np.array(label, dtype=np.uint8)).long()</code></pre>
<h4 id="trainpy">train.py</h4>
<pre><code class="language-python">import os
import argparse
import torch
from torch.utils.data import DataLoader, WeightedRandomSampler
from tqdm import tqdm
from collections import OrderedDict
import json
from pathlib import Path

from DDRNet import DDRNet
from functions import *

def arg_as_dict(s):
    try:
        return json.loads(s)
    except Exception as e:
        raise argparse.ArgumentTypeError(f&quot;Argument must be a JSON-formatted dictionary string. Error: {e}&quot;)

def train_and_validate(args):
    device = torch.device(f&quot;cuda:{args.gpu_id}&quot; if torch.cuda.is_available() else &quot;cpu&quot;)
    print(f&quot;Initialized training on device: {device}&quot;)

    # 데이터셋 폴더별 가중치를 위한 설정
    train_sub_folders = [&#39;cam0&#39;, &#39;cam1&#39;, &#39;cam2&#39;, &#39;cam3&#39;, &#39;cam4&#39;, &#39;cam5&#39;, &#39;set1&#39;, &#39;set2&#39;, &#39;set3&#39;]
    val_sub_folders = [&#39;cam0&#39;, &#39;cam1&#39;, &#39;cam2&#39;, &#39;cam3&#39;, &#39;cam4&#39;, &#39;cam5&#39;, &#39;set1&#39;, &#39;set2&#39;, &#39;set3&#39;]

    train_dataset = SegmentationDataset(args.dataset_dir, args.crop_size, &#39;train&#39;, args.scale_range, sub_folders=train_sub_folders)

    # --- 폴더별 가중치에 따른 샘플링 확률 계산 ---
    if args.folder_weights:
        print(&quot;Applying folder-wise weights for sampling...&quot;)
        folder_indices = [sample[1] for sample in train_dataset.samples]
        folder_names_per_sample = [train_sub_folders[i] for i in folder_indices]
        sample_weights = [args.folder_weights.get(name, 1.0) for name in folder_names_per_sample]
        print(f&quot;Sample weights will be based on folder weights: {args.folder_weights}&quot;)
        sampler = WeightedRandomSampler(weights=sample_weights, num_samples=len(sample_weights), replacement=True)
        shuffle = False
    else:
        sampler = None
        shuffle = True

    val_dataset = SegmentationDataset(args.dataset_dir, args.crop_size, &#39;val&#39;, args.scale_range, sub_folders=val_sub_folders)

    train_dataloader = DataLoader(train_dataset, batch_size=args.batch_size, shuffle=shuffle, sampler=sampler, num_workers=args.num_workers, pin_memory=True, drop_last=True)
    val_dataloader = DataLoader(val_dataset, batch_size=args.batch_size, shuffle=False, num_workers=args.num_workers, pin_memory=True)

    model = DDRNet(num_classes=args.num_classes).to(device)

    class_weights = None
    if args.class_weights:
        if len(args.class_weights) != args.num_classes:
            raise ValueError(f&quot;Number of class_weights ({len(args.class_weights)}) must match num_classes ({args.num_classes})&quot;)
        print(f&quot;Applying class weights: {args.class_weights}&quot;)
        class_weights = torch.tensor(args.class_weights, dtype=torch.float).to(device)

    if args.use_ohem:
        print(&quot;Using OhemCrossEntropy Loss.&quot;)
        criterion = OhemCrossEntropy(ignore_label=255, weight=class_weights)
    else:
        print(&quot;Using standard CrossEntropy Loss.&quot;)
        criterion = CrossEntropy(ignore_label=255, weight=class_weights)

    optimizer = torch.optim.SGD(model.parameters(), lr=args.lr, momentum=args.momentum, weight_decay=args.weight_decay)
    scheduler = WarmupPolyEpochLR(optimizer, total_epochs=args.epochs, warmup_epochs=args.warmup_epochs)

    start_epoch = 0
    min_val_loss = float(&#39;inf&#39;)
    if args.loadpath:
        print(f&quot;Loading checkpoint from: {args.loadpath}&quot;)
        checkpoint = torch.load(args.loadpath, map_location=device)
        try:
            # DDP 학습 가중치(&#39;module.&#39; 접두사) 호환을 위한 처리
            new_state_dict = OrderedDict()
            for k, v in checkpoint[&#39;model_state_dict&#39;].items():
                name = k[7:] if k.startswith(&#39;module.&#39;) else k
                new_state_dict[name] = v
            model.load_state_dict(new_state_dict, strict=False)

            optimizer.load_state_dict(checkpoint[&#39;optimizer_state_dict&#39;])
            scheduler.load_state_dict(checkpoint[&#39;scheduler_state_dict&#39;])
            start_epoch = checkpoint[&#39;epoch&#39;] + 1
            min_val_loss = checkpoint.get(&#39;loss&#39;, float(&#39;inf&#39;))
            print(f&quot;Resuming training from epoch {start_epoch}, with min_val_loss: {min_val_loss:.4f}&quot;)
        except KeyError:
            print(&quot;Old checkpoint format. Loading model state_dict only.&quot;)
            load_state_dict(model, checkpoint)

    os.makedirs(args.result_dir, exist_ok=True)
    log_path = os.path.join(args.result_dir, &quot;log.txt&quot;)
    with open(log_path, &#39;a&#39; if start_epoch &gt; 0 else &#39;w&#39;) as f:
        if start_epoch == 0:
            f.write(&quot;Epoch\t\tTrain-loss\t\tVal-loss\t\tlearningRate\n&quot;)

    for epoch in range(start_epoch, args.epochs):
        model.train()
        total_train_loss = 0.0
        loop = tqdm(train_dataloader, desc=f&quot;Train [{epoch+1}/{args.epochs}]&quot;, ncols=100)

        for i, (imgs, labels) in enumerate(loop):
            optimizer.zero_grad(set_to_none=True)
            imgs, labels = imgs.to(device), labels.to(device)
            outputs = model(imgs)
            loss = criterion(outputs, labels)
            loss.backward()
            optimizer.step()
            total_train_loss += loss.item()
            loop.set_postfix(loss=loss.item(), avg_loss=total_train_loss/(i+1), lr=scheduler.get_last_lr()[0])

        avg_train_loss = total_train_loss / len(train_dataloader)
        scheduler.step()

        avg_val_loss_str = &quot;N/A&quot;
        if (epoch + 1) % 5 == 0 or (epoch + 1) == args.epochs:
            model.eval()
            total_val_loss = 0.0
            with torch.no_grad():
                loop_val = tqdm(val_dataloader, desc=f&quot;Val [{epoch+1}/{args.epochs}]&quot;, ncols=100)
                for imgs, labels in loop_val:
                    imgs, labels = imgs.to(device), labels.to(device)
                    outputs = model(imgs)
                    loss = criterion(outputs, labels)
                    total_val_loss += loss.item()

            avg_val_loss = total_val_loss / len(val_dataloader)
            avg_val_loss_str = f&quot;{avg_val_loss:.4f}&quot;
            print(f&quot;\nEpoch {epoch+1}: Train Loss = {avg_train_loss:.4f}, Validation Loss = {avg_val_loss:.4f}&quot;)

            if avg_val_loss &lt; min_val_loss:
                min_val_loss = avg_val_loss
                best_path = os.path.join(args.result_dir, &quot;model_best.pth&quot;)
                torch.save({&#39;model_state_dict&#39;: model.state_dict()}, best_path)
                print(f&quot;Best model saved at epoch {epoch+1} with val loss {min_val_loss:.4f}&quot;)

        lr = scheduler.get_last_lr()[0]
        with open(log_path, &quot;a&quot;) as f:
            f.write(f&quot;\n{epoch + 1}\t\t{avg_train_loss:.4f}\t\t{avg_val_loss_str}\t\t{lr:.8f}&quot;)

if __name__ == &quot;__main__&quot;:
    parser = argparse.ArgumentParser(description=&quot;DDRNet Weighted Training Script&quot;)

    parser.add_argument(&quot;--dataset_dir&quot;, type=str, default=&quot;./data&quot;)
    parser.add_argument(&quot;--result_dir&quot;, type=str, default=&quot;output&quot;)
    parser.add_argument(&quot;--loadpath&quot;, type=str, default=None)
    parser.add_argument(&quot;--epochs&quot;, type=int, default=400)
    parser.add_argument(&quot;--num_classes&quot;, type=int, default=19)
    parser.add_argument(&quot;--gpu_id&quot;, type=int, default=0)

    parser.add_argument(&quot;--lr&quot;, type=float, default=1e-2)
    parser.add_argument(&quot;--batch_size&quot;, type=int, default=8)
    parser.add_argument(&quot;--momentum&quot;, type=float, default=0.9)
    parser.add_argument(&quot;--weight_decay&quot;, type=float, default=5e-4)
    parser.add_argument(&quot;--warmup_epochs&quot;, type=int, default=5)

    parser.add_argument(&quot;--crop_size&quot;, default=[512, 1024], type=arg_as_list)
    parser.add_argument(&quot;--scale_range&quot;, default=[0.75, 1.5], type=arg_as_list)
    parser.add_argument(&quot;--num_workers&quot;, type=int, default=os.cpu_count())

    # 폴더 가중치 조절. (1.0이 기본 가중치)
    parser.add_argument(&quot;--folder_weights&quot;, type=arg_as_dict, default={&quot;cam0&quot;:1.0, &quot;cam1&quot;:1.0, &quot;cam2&quot;:1.0, &quot;cam3&quot;:0.8, &quot;cam4&quot;:1.0, &quot;cam5&quot;:1.0, &quot;set1&quot;:1.5, &quot;set2&quot;:1.8, &quot;set3&quot;:1.5},
                        help=&#39;{&quot;cam0&quot;: 1.0, &quot;set1&quot;: 2.0}&#39;)
    # 클래스 가중치 조절 (1.0이 기본 가중치로 픽셀 수에 따라서 조절.)
    parser.add_argument(&quot;--class_weights&quot;, type=arg_as_list, default=[2.0166, 3.481, 4.0911, 3.9912, 3.9619, 2.0864, 1.8396, 4.3168, 3.79, 6.4674, 5.7661, 5.642, 8.4116, 5.9525, 2.2137, 5.2137, 6.1661, 4.195, 1.0],
                        help=&#39;List of weights for each class. &quot;[1.0, 1.5, 0.8]&quot;&#39;)

    parser.add_argument(&quot;--use_ohem&quot;, action=&#39;store_true&#39;, help=&quot;Use OHEM Cross Entropy loss&quot;)

    args = parser.parse_args()

    result_dir = Path(args.result_dir)
    result_dir.mkdir(parents=True, exist_ok=True)

    train_and_validate(args)</code></pre>
<h3 id="학습-실행-최종-명령어">학습 실행 최종 명령어.</h3>
<blockquote>
<p>python train_weight.py --loadpath ./DDRNet_cityscape.pth --epoch 300  --batch_size 16</p>
</blockquote>
<h3 id="학습-결과">학습 결과</h3>
<p>클래스 &amp; 폴더별 | 클래스 | 폴더별
|---|---|---|
| <img src="https://velog.velcdn.com/images/chan_woo_00/post/924710fd-376a-4075-b518-18282aba41c0/image.png" alt=""> | <img src="https://velog.velcdn.com/images/chan_woo_00/post/0014b132-905b-44ce-b8da-6c4baa414f3c/image.png" alt=""> | <img src="https://velog.velcdn.com/images/chan_woo_00/post/96a624e4-ba0c-408b-95c6-24556112016e/image.png" alt=""> |</p>
<h4 id="test-dataset-predict-결과">test dataset predict 결과</h4>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/chan_woo_00/post/086f0937-a648-46db-804b-2c33d996430b/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/chan_woo_00/post/62205a0c-d427-4990-a77a-347a144bb046/image.png" alt=""></th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/chan_woo_00/post/9dffe19e-a1f0-4eff-b905-8e42e3435c83/image.jpg" alt=""></td>
<td><img src="https://velog.velcdn.com/images/chan_woo_00/post/25dd66b7-2139-4ce8-bf0c-03ff4598eedb/image.jpg" alt=""></td>
</tr>
</tbody></table>
<h4 id="inference-time">inference time</h4>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/c70fc1c7-3c64-4197-a558-b94d0aaeef33/image.png" alt="">
모델 구조에 대해서 현재 환경에 약 9.3ms소요</p>
<h3 id="결과-종합">결과 종합</h3>
<p>모델명: DDRNet</p>
<p>데이터셋 : 제공된 데이터셋 <code>7 : 2 : 1</code>로 분할시켜 학습 및 테스트 진행</p>
<pre><code class="language-python">import os
import shutil
from pathlib import Path

def split_dataset(base_dir):
    main_folders = [&#39;colormap&#39;, &#39;image&#39;, &#39;labelmap&#39;]

    source_base_path = Path(base_dir) / &#39;image&#39; / &#39;train&#39;

    if not source_base_path.exists():
        return

    # train 폴더 내의 하위 폴더들(cam0, cam1, set1 등) 목록 가져오기
    try:
        sub_folders = [d.name for d in source_base_path.iterdir() if d.is_dir()]
    except OSError as e:
        print(f&quot;문제가 발생했습니다&quot;)
        return

    print(&quot;데이터셋 분할을 시작&quot;)

    for sub_folder in sub_folders:
        print(f&quot;\n📁 [{sub_folder}] 폴더 처리 중...&quot;)

        source_sub_folder_path = source_base_path / sub_folder

        try:
            files = sorted([f.name for f in source_sub_folder_path.iterdir() if f.is_file()])
        except FileNotFoundError:
            print(f&quot;  &#39;{source_sub_folder_path}&#39; 폴더를 찾을 수 없습니다..&quot;)
            continue

        if not files:
            print(f&quot;  &#39;{sub_folder}&#39; 폴더에 파일이 없습니다.&quot;)
            continue

        for main_folder in main_folders:
            for split_type in [&#39;train&#39;, &#39;val&#39;, &#39;test&#39;]:
                dest_path = Path(base_dir) / main_folder / split_type / sub_folder
                dest_path.mkdir(parents=True, exist_ok=True)

        moved_counts = {&#39;train&#39;: 0, &#39;val&#39;: 0, &#39;test&#39;: 0}
        for i in range(0, len(files), 10):
            chunk = files[i:i+10]

            # 10개 미만이면 train으로 이동
            if len(chunk) &lt; 10:
                split_map = {&#39;train&#39;: chunk}
            # 10개이면 7:2:1로 분할
            else:
                split_map = {
                    &#39;train&#39;: chunk[0:7],
                    &#39;val&#39;: chunk[7:9],
                    &#39;test&#39;: chunk[9:10]
                }
            for split_type, files_to_move in split_map.items():
                if not files_to_move:
                    continue

                for file_name in files_to_move:
                    moved_counts[split_type] += 1
                    for main_folder in main_folders:
                        source_file = Path(base_dir) / main_folder / &#39;train&#39; / sub_folder / file_name
                        dest_file = Path(base_dir) / main_folder / split_type / sub_folder / file_name

                        if source_file.exists():
                            shutil.move(str(source_file), str(dest_file))

        print(f&quot;  - ✅ Train: {moved_counts[&#39;train&#39;]}개 파일 이동 완료&quot;)
        print(f&quot;  - ✅ Validation: {moved_counts[&#39;val&#39;]}개 파일 이동 완료&quot;)
        print(f&quot;  - ✅ Test: {moved_counts[&#39;test&#39;]}개 파일 이동 완료&quot;)


if __name__ == &#39;__main__&#39;:
    base_directory = &#39;C:/etri/data&#39;  

    split_dataset(base_directory)</code></pre>
<p>테스트 코드</p>
<h4 id="prediction">prediction</h4>
<pre><code class="language-python">import os
import argparse
from glob import glob
from PIL import Image
import numpy as np
from tqdm import tqdm
import torch
import torch.nn.functional as F
from torchvision import transforms
from DDRNet import DDRNet
from torch.utils.data import Dataset, DataLoader
import matplotlib.cm as cm
from collections import OrderedDict


class TestSegmentationDataset(Dataset):
    def __init__(self, root_dir, subset=&#39;test&#39;):
        self.image_dir = os.path.join(root_dir, &quot;image&quot;, subset)
        self.image_paths = sorted(glob(os.path.join(self.image_dir, &quot;*&quot;, &quot;*.*&quot;), recursive=True))
        self.to_tensor = transforms.ToTensor()

    def __len__(self):
        return len(self.image_paths)

    def __getitem__(self, idx):
        img_path = self.image_paths[idx]
        img = Image.open(img_path).convert(&quot;RGB&quot;)
        tensor = self.to_tensor(img)
        return tensor, img_path

# 단일 GPU
def load_model(weight_path, num_classes, device):
    model = DDRNet(num_classes=num_classes)

    checkpoint = torch.load(weight_path, map_location=device)
    if &#39;model_state_dict&#39; in checkpoint:
        state_dict = checkpoint[&#39;model_state_dict&#39;]
    else:
        state_dict = checkpoint

    new_state_dict = OrderedDict()
    for k, v in state_dict.items():
        name = k[7:] if k.startswith(&#39;module.&#39;) else k # &#39;module.&#39; 접두사를 제거
        new_state_dict[name] = v

    model.load_state_dict(new_state_dict)

    model = model.to(device)
    model.eval()

    return model

# 예측 결과를 이미지 파일로 저장
def save_prediction(pred, save_path, colormap_root, num_classes):
    pred_np = pred.squeeze().cpu().numpy().astype(np.uint8)

    os.makedirs(os.path.dirname(save_path), exist_ok=True)
    Image.fromarray(pred_np).save(save_path)

    normed = pred_np.astype(np.float32) / (num_classes - 1) 
    cmap = cm.get_cmap(&#39;turbo&#39;)
    colored = cmap(normed)
    rgb = (colored[:, :, :3] * 255).astype(np.uint8)
    rgb_img = Image.fromarray(rgb)

    try:
        rel_path = os.path.relpath(save_path, start=os.path.dirname(save_path))
        cmap_path = os.path.join(colormap_root, os.path.dirname(os.path.relpath(save_path, start=args.result_dir)), rel_path)
    except ValueError: # 다른 드라이브에 있을 경우 대비
        rel_path = Path(save_path).name
        cmap_path = os.path.join(colormap_root, rel_path)

    os.makedirs(os.path.dirname(cmap_path), exist_ok=True)
    rgb_img.save(cmap_path)

# 전체 테스트
def test(args):
    # 단일 GPU 사용
    device = torch.device(&quot;cuda:0&quot; if torch.cuda.is_available() else &quot;cpu&quot;)
    print(f&quot;Using device: {device}&quot;)

    dataset = TestSegmentationDataset(args.dataset_dir, subset=args.subset)
    if not dataset.image_paths:
        print(f&quot;Error: No images found in &#39;{dataset.image_dir}&#39;. Please check the path and subset.&quot;)
        return

    dataloader = DataLoader(dataset, batch_size=1, shuffle=False, num_workers=2)

    model = load_model(args.weight_path, args.num_classes, device)
    colormap_root = os.path.join(args.result_dir, &quot;colormap&quot;)

    with torch.inference_mode():
        for img_tensor, img_path_tuple in tqdm(dataloader, desc=&quot;Predicting...&quot;):
            img_path = img_path_tuple[0]
            img_tensor = img_tensor.to(device)

            output = model(img_tensor)
            if isinstance(output, tuple):
                output = output[0]

            pred = torch.argmax(output, dim=1)

            rel_path = os.path.relpath(img_path, start=os.path.join(args.dataset_dir, &quot;image&quot;))
            save_path = os.path.join(args.result_dir, rel_path)

            save_prediction(pred, save_path, colormap_root, args.num_classes)

if __name__ == &quot;__main__&quot;:
    parser = argparse.ArgumentParser()
    parser.add_argument(&quot;--dataset_dir&quot;, type=str, default=&quot;./data&quot;, help=&quot;Path to dataset root directory&quot;)
    parser.add_argument(&quot;--weight_path&quot;, type=str, default=&quot;./output/model_best.pth&quot;, help=&quot;Path to model weight (.pth)&quot;)
    parser.add_argument(&quot;--result_dir&quot;, type=str, default=&quot;./result&quot;, help=&quot;Directory to save results&quot;)
    parser.add_argument(&quot;--num_classes&quot;, type=int, default=19, help=&quot;Number of segmentation classes&quot;)
    parser.add_argument(&quot;--subset&quot;, type=str, default=&quot;test&quot;, help=&quot;Which subset to run prediction on (e.g., &#39;test&#39;, &#39;val&#39;, &#39;train&#39;)&quot;)

    args = parser.parse_args()

    os.makedirs(args.result_dir, exist_ok=True)

    test(args)</code></pre>
<h4 id="miou-계산">mIoU 계산</h4>
<pre><code class="language-python">import os
import argparse
import numpy as np
from PIL import Image
from glob import glob
from tqdm import tqdm
from sklearn.metrics import confusion_matrix
from pathlib import Path

def load_image(path):
    return np.array(Image.open(path)).astype(np.uint8)

def compute_miou(confusion, num_classes):
    &quot;&quot;&quot;
    1. mIoU (All): NaN을 제외한 모든 클래스(IoU=0 포함)의 평균
    2. mIoU (&gt;0): IoU가 0보다 큰 클래스들만의 평균
    &quot;&quot;&quot;
    ious = []
    for cls in range(num_classes):
        TP = confusion[cls, cls]
        FP = confusion[:, cls].sum() - TP
        FN = confusion[cls, :].sum() - TP

        denom = TP + FP + FN
        if denom == 0:
            iou = float(&#39;nan&#39;)
        else:
            iou = TP / denom
        ious.append(iou)

    # mIoU (All Classes) 계산
    # NaN 값을 무시하고 평균을 계산.
    miou_all = np.nanmean(ious)

    # mIoU (IoU &gt; 0 Classes Only) 계산
    # IoU가 NaN이 아니고 0보다 큰 값들만 계산.
    positive_ious = [iou for iou in ious if not np.isnan(iou) and iou &gt; 0]

    # 0이 제거된 iou 값들의 평균을 계산
    if not positive_ious:
        miou_positive = 0.0
    else:
        miou_positive = np.mean(positive_ious) 

    return miou_all, miou_positive, ious

def evaluate(result_dir, label_dir, num_classes):
    pred_paths = sorted(glob(os.path.join(result_dir, &quot;**&quot;, &quot;*_leftImg8bit.png&quot;), recursive=True))
    print(f&#39;Found {len(pred_paths)} segmentation result images in {result_dir}&#39;)

    if not pred_paths:
        print(&quot;Error: No prediction files found. Please check the &#39;result_dir&#39; path and file names.&quot;)
        return

    all_confusion = np.zeros((num_classes, num_classes), dtype=np.int64)

    for pred_path in tqdm(pred_paths, desc=&quot;Evaluating&quot;):
        sub_folder = Path(pred_path).parent.name
        file_id = os.path.basename(pred_path).replace(&quot;_leftImg8bit.png&quot;, &quot;&quot;)

        label_path = os.path.join(label_dir, sub_folder, f&quot;{file_id}_gtFine_CategoryId.png&quot;)

        if not os.path.exists(label_path):
            print(f&quot;Label not found at {label_path}, skipping.&quot;)
            continue

        pred = load_image(pred_path).flatten()
        label = load_image(label_path).flatten()

        mask = label != 255
        pred = pred[mask]
        label = label[mask]

        pred = np.clip(pred, 0, num_classes - 1)
        label = np.clip(label, 0, num_classes - 1)

        conf = confusion_matrix(label, pred, labels=list(range(num_classes)))
        all_confusion += conf

    miou_all, miou_positive, ious = compute_miou(all_confusion, num_classes)

    print(&quot;\n--- Evaluation Results ---&quot;)
    print(f&quot;📊 mIoU (All Classes, IoU=0 포함): {miou_all:.4f}&quot;)
    print(f&quot;📊 mIoU (Positive Classes, IoU&gt;0 제외): {miou_positive:.4f}&quot;)
    print(&quot;--------------------------&quot;)

    for i, iou in enumerate(ious):
        print(f&quot;Class {i}: IoU = {iou:.4f}&quot; if not np.isnan(iou) else f&quot;Class {i}: IoU = NaN (ignored in mean)&quot;)

if __name__ == &quot;__main__&quot;:
    parser = argparse.ArgumentParser(description=&quot;Calculate mIoU for semantic segmentation results.&quot;)

    parser.add_argument(&quot;--result_dir&quot;, type=str, default=&quot;C:/ETRI/result/test&quot;, 
                        help=&quot;Predicted *_leftImg8bit.png files가 있는 상위 디렉토리&quot;)
    parser.add_argument(&quot;--label_dir&quot;, type=str, default=&quot;C:/ETRI/data/labelmap/test&quot;, 
                        help=&quot;정답 레이블 *_gtFine_CategoryId.png files가 있는 상위 디렉토리&quot;)
    parser.add_argument(&quot;--num_classes&quot;, type=int, default=19, help=&quot;세그먼테이션 클래스 수&quot;)

    args = parser.parse_args()

    evaluate(args.result_dir, args.label_dir, args.num_classes)</code></pre>
<p>클래스 &amp; 폴더별 | 클래스 | 폴더별
|---|---|---|
| <img src="https://velog.velcdn.com/images/chan_woo_00/post/924710fd-376a-4075-b518-18282aba41c0/image.png" alt=""> | <img src="https://velog.velcdn.com/images/chan_woo_00/post/0014b132-905b-44ce-b8da-6c4baa414f3c/image.png" alt=""> | <img src="https://velog.velcdn.com/images/chan_woo_00/post/96a624e4-ba0c-408b-95c6-24556112016e/image.png" alt=""> |</p>
<blockquote>
<p>개선방법 1 : ** 데이터 증식 추가**
개선방법 2 : 손실함수 변경</p>
</blockquote>
<table>
<thead>
<tr>
<th></th>
<th align="center">baseline</th>
<th align="center">DDRNet_weight</th>
<th align="center">DDRNet_class_weight</th>
<th align="center">DDRNet_folder_weight</th>
</tr>
</thead>
<tbody><tr>
<td>mIoU</td>
<td align="center">0.3228</td>
<td align="center">0.3528</td>
<td align="center">0.4502</td>
<td align="center">0.3581</td>
</tr>
<tr>
<td>inference time</td>
<td align="center">9.375ms</td>
<td align="center">9.375ms</td>
<td align="center">9.375ms</td>
<td align="center">9.375ms</td>
</tr>
</tbody></table>
<h3 id="ddrnet에-대한-개선-방향">DDRNet에 대한 개선 방향</h3>
<ul>
<li>데이터 증식 방법 조절(우천에 최적화된 방향으로)</li>
<li>손실함수 변경(weighted CE)</li>
<li>mIoU계산에 대한 조절(test방향과 많이 다른 것을 확인 - 50 mIoU가 실제 test에서는 33점대로 떨어져 cam0~cam5에 대해서 학습이 실제 test 데이터셋과는 많이 차이가 나는것을 확인.)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[DDRNet 가중치 조절]]></title>
            <link>https://velog.io/@chan_woo_00/DDRNet-%EA%B0%80%EC%A4%91%EC%B9%98-%EC%A1%B0%EC%A0%88</link>
            <guid>https://velog.io/@chan_woo_00/DDRNet-%EA%B0%80%EC%A4%91%EC%B9%98-%EC%A1%B0%EC%A0%88</guid>
            <pubDate>Sun, 28 Sep 2025 17:22:33 GMT</pubDate>
            <description><![CDATA[<h2 id="ddrnet-가중치-조절">DDRNet 가중치 조절</h2>
<h3 id="환경-구성">환경 구성.</h3>
<p>중요 환경에 대한 버전.</p>
<pre><code class="language-ssss">Package             Version
------------------- --------------------
numpy               1.24.1
opencv-python       4.12.0.88
thop                0.1.1.post2209072238
torch               2.3.1+cu118
torchaudio          2.3.1+cu118
torchvision         0.18.1+cu118</code></pre>
<h3 id="진행-작업">진행 작업</h3>
<blockquote>
<ol>
<li>폴더별 가중치 조절 코드 추가.</li>
<li>클래스별 가중치 조절 코드 추가.</li>
<li>기초 데이터 증식 코드 추가.</li>
</ol>
</blockquote>
<pre><code class="language-python">class SegmentationTransform:
    def __init__(self, crop_size=[1024, 1024], scale_range=[0.5, 1.5]):
        self.crop_size = crop_size
        self.scale_range = scale_range
        self.mean = [0.485, 0.456, 0.406]
        self.std = [0.229, 0.224, 0.225]
        self.bilinear = transforms.InterpolationMode.BILINEAR
        self.nearest = transforms.InterpolationMode.NEAREST

        # Color Jitter
        self.color_jitter = transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.3, hue=0.1)
        # Gaussian Blur
        self.gaussian_blur = transforms.GaussianBlur(kernel_size=(5, 9), sigma=(0.1, 5))

    def __call__(self, image, label):
        scale_factor = random.uniform(self.scale_range[0], self.scale_range[1])
        width, height = image.size
        new_width, new_height = int(width * scale_factor), int(height * scale_factor)
        image = TF.resize(image, (new_height, new_width), interpolation=self.bilinear)
        label = TF.resize(label, (new_height, new_width), interpolation=self.nearest)

        pad_h = max(self.crop_size[0] - new_height, 0)
        pad_w = max(self.crop_size[1] - new_width, 0)
        if pad_h &gt; 0 or pad_w &gt; 0:
            padding = (0, 0, pad_w, pad_h)
            image = TF.pad(image, padding, fill=0)
            label = TF.pad(label, padding, fill=255)

        # 크롭
        i, j, h, w = transforms.RandomCrop.get_params(image, output_size=self.crop_size)
        image = TF.crop(image, i, j, h, w)
        label = TF.crop(label, i, j, h, w)

        # 좌우 반전
        if random.random() &gt; 0.3:
            image = TF.hflip(image)
            label = TF.hflip(label)

        # 회전
        if random.random() &gt; 0.5:
            angle = random.uniform(-5, 5)
            image = TF.rotate(image, angle, interpolation=self.bilinear, fill=0)
            label = TF.rotate(label, angle, interpolation=self.nearest, fill=255)

        # 색상 변환
        if random.random() &gt; 0.4:
            image = self.color_jitter(image)
        if random.random() &gt; 0.3:
            image = self.gaussian_blur(image)

        image = TF.to_tensor(image)
        image = TF.normalize(image, mean=self.mean, std=self.std)
        label = torch.from_numpy(np.array(label, dtype=np.uint8)).long()</code></pre>
<h4 id="trainpy">train.py</h4>
<pre><code class="language-python">import os
import argparse
import torch
from torch.utils.data import DataLoader, WeightedRandomSampler
from tqdm import tqdm
from collections import OrderedDict
import json
from pathlib import Path

from DDRNet import DDRNet
from functions import *

def arg_as_dict(s):
    try:
        return json.loads(s)
    except Exception as e:
        raise argparse.ArgumentTypeError(f&quot;Argument must be a JSON-formatted dictionary string. Error: {e}&quot;)

def train_and_validate(args):
    device = torch.device(f&quot;cuda:{args.gpu_id}&quot; if torch.cuda.is_available() else &quot;cpu&quot;)
    print(f&quot;Initialized training on device: {device}&quot;)

    # 데이터셋 폴더별 가중치를 위한 설정
    train_sub_folders = [&#39;cam0&#39;, &#39;cam1&#39;, &#39;cam2&#39;, &#39;cam3&#39;, &#39;cam4&#39;, &#39;cam5&#39;, &#39;set1&#39;, &#39;set2&#39;, &#39;set3&#39;]
    val_sub_folders = [&#39;cam0&#39;, &#39;cam1&#39;, &#39;cam2&#39;, &#39;cam3&#39;, &#39;cam4&#39;, &#39;cam5&#39;, &#39;set1&#39;, &#39;set2&#39;, &#39;set3&#39;]

    train_dataset = SegmentationDataset(args.dataset_dir, args.crop_size, &#39;train&#39;, args.scale_range, sub_folders=train_sub_folders)

    # --- 폴더별 가중치에 따른 샘플링 확률 계산 ---
    if args.folder_weights:
        print(&quot;Applying folder-wise weights for sampling...&quot;)
        folder_indices = [sample[1] for sample in train_dataset.samples]
        folder_names_per_sample = [train_sub_folders[i] for i in folder_indices]
        sample_weights = [args.folder_weights.get(name, 1.0) for name in folder_names_per_sample]
        print(f&quot;Sample weights will be based on folder weights: {args.folder_weights}&quot;)
        sampler = WeightedRandomSampler(weights=sample_weights, num_samples=len(sample_weights), replacement=True)
        shuffle = False
    else:
        sampler = None
        shuffle = True

    val_dataset = SegmentationDataset(args.dataset_dir, args.crop_size, &#39;val&#39;, args.scale_range, sub_folders=val_sub_folders)

    train_dataloader = DataLoader(train_dataset, batch_size=args.batch_size, shuffle=shuffle, sampler=sampler, num_workers=args.num_workers, pin_memory=True, drop_last=True)
    val_dataloader = DataLoader(val_dataset, batch_size=args.batch_size, shuffle=False, num_workers=args.num_workers, pin_memory=True)

    model = DDRNet(num_classes=args.num_classes).to(device)

    class_weights = None
    if args.class_weights:
        if len(args.class_weights) != args.num_classes:
            raise ValueError(f&quot;Number of class_weights ({len(args.class_weights)}) must match num_classes ({args.num_classes})&quot;)
        print(f&quot;Applying class weights: {args.class_weights}&quot;)
        class_weights = torch.tensor(args.class_weights, dtype=torch.float).to(device)

    if args.use_ohem:
        print(&quot;Using OhemCrossEntropy Loss.&quot;)
        criterion = OhemCrossEntropy(ignore_label=255, weight=class_weights)
    else:
        print(&quot;Using standard CrossEntropy Loss.&quot;)
        criterion = CrossEntropy(ignore_label=255, weight=class_weights)

    optimizer = torch.optim.SGD(model.parameters(), lr=args.lr, momentum=args.momentum, weight_decay=args.weight_decay)
    scheduler = WarmupPolyEpochLR(optimizer, total_epochs=args.epochs, warmup_epochs=args.warmup_epochs)

    start_epoch = 0
    min_val_loss = float(&#39;inf&#39;)
    if args.loadpath:
        print(f&quot;Loading checkpoint from: {args.loadpath}&quot;)
        checkpoint = torch.load(args.loadpath, map_location=device)
        try:
            # DDP 학습 가중치(&#39;module.&#39; 접두사) 호환을 위한 처리
            new_state_dict = OrderedDict()
            for k, v in checkpoint[&#39;model_state_dict&#39;].items():
                name = k[7:] if k.startswith(&#39;module.&#39;) else k
                new_state_dict[name] = v
            model.load_state_dict(new_state_dict, strict=False)

            optimizer.load_state_dict(checkpoint[&#39;optimizer_state_dict&#39;])
            scheduler.load_state_dict(checkpoint[&#39;scheduler_state_dict&#39;])
            start_epoch = checkpoint[&#39;epoch&#39;] + 1
            min_val_loss = checkpoint.get(&#39;loss&#39;, float(&#39;inf&#39;))
            print(f&quot;Resuming training from epoch {start_epoch}, with min_val_loss: {min_val_loss:.4f}&quot;)
        except KeyError:
            print(&quot;Old checkpoint format. Loading model state_dict only.&quot;)
            load_state_dict(model, checkpoint)

    os.makedirs(args.result_dir, exist_ok=True)
    log_path = os.path.join(args.result_dir, &quot;log.txt&quot;)
    with open(log_path, &#39;a&#39; if start_epoch &gt; 0 else &#39;w&#39;) as f:
        if start_epoch == 0:
            f.write(&quot;Epoch\t\tTrain-loss\t\tVal-loss\t\tlearningRate\n&quot;)

    for epoch in range(start_epoch, args.epochs):
        model.train()
        total_train_loss = 0.0
        loop = tqdm(train_dataloader, desc=f&quot;Train [{epoch+1}/{args.epochs}]&quot;, ncols=100)

        for i, (imgs, labels) in enumerate(loop):
            optimizer.zero_grad(set_to_none=True)
            imgs, labels = imgs.to(device), labels.to(device)
            outputs = model(imgs)
            loss = criterion(outputs, labels)
            loss.backward()
            optimizer.step()
            total_train_loss += loss.item()
            loop.set_postfix(loss=loss.item(), avg_loss=total_train_loss/(i+1), lr=scheduler.get_last_lr()[0])

        avg_train_loss = total_train_loss / len(train_dataloader)
        scheduler.step()

        avg_val_loss_str = &quot;N/A&quot;
        if (epoch + 1) % 5 == 0 or (epoch + 1) == args.epochs:
            model.eval()
            total_val_loss = 0.0
            with torch.no_grad():
                loop_val = tqdm(val_dataloader, desc=f&quot;Val [{epoch+1}/{args.epochs}]&quot;, ncols=100)
                for imgs, labels in loop_val:
                    imgs, labels = imgs.to(device), labels.to(device)
                    outputs = model(imgs)
                    loss = criterion(outputs, labels)
                    total_val_loss += loss.item()

            avg_val_loss = total_val_loss / len(val_dataloader)
            avg_val_loss_str = f&quot;{avg_val_loss:.4f}&quot;
            print(f&quot;\nEpoch {epoch+1}: Train Loss = {avg_train_loss:.4f}, Validation Loss = {avg_val_loss:.4f}&quot;)

            if avg_val_loss &lt; min_val_loss:
                min_val_loss = avg_val_loss
                best_path = os.path.join(args.result_dir, &quot;model_best.pth&quot;)
                torch.save({&#39;model_state_dict&#39;: model.state_dict()}, best_path)
                print(f&quot;Best model saved at epoch {epoch+1} with val loss {min_val_loss:.4f}&quot;)

        lr = scheduler.get_last_lr()[0]
        with open(log_path, &quot;a&quot;) as f:
            f.write(f&quot;\n{epoch + 1}\t\t{avg_train_loss:.4f}\t\t{avg_val_loss_str}\t\t{lr:.8f}&quot;)

if __name__ == &quot;__main__&quot;:
    parser = argparse.ArgumentParser(description=&quot;DDRNet Weighted Training Script&quot;)

    parser.add_argument(&quot;--dataset_dir&quot;, type=str, default=&quot;./data&quot;)
    parser.add_argument(&quot;--result_dir&quot;, type=str, default=&quot;output&quot;)
    parser.add_argument(&quot;--loadpath&quot;, type=str, default=None)
    parser.add_argument(&quot;--epochs&quot;, type=int, default=400)
    parser.add_argument(&quot;--num_classes&quot;, type=int, default=19)
    parser.add_argument(&quot;--gpu_id&quot;, type=int, default=0)

    parser.add_argument(&quot;--lr&quot;, type=float, default=1e-2)
    parser.add_argument(&quot;--batch_size&quot;, type=int, default=8)
    parser.add_argument(&quot;--momentum&quot;, type=float, default=0.9)
    parser.add_argument(&quot;--weight_decay&quot;, type=float, default=5e-4)
    parser.add_argument(&quot;--warmup_epochs&quot;, type=int, default=5)

    parser.add_argument(&quot;--crop_size&quot;, default=[512, 1024], type=arg_as_list)
    parser.add_argument(&quot;--scale_range&quot;, default=[0.75, 1.5], type=arg_as_list)
    parser.add_argument(&quot;--num_workers&quot;, type=int, default=os.cpu_count())

    # 폴더 가중치 조절. (1.0이 기본 가중치)
    parser.add_argument(&quot;--folder_weights&quot;, type=arg_as_dict, default={&quot;cam0&quot;:1.0, &quot;cam1&quot;:1.0, &quot;cam2&quot;:1.0, &quot;cam3&quot;:0.8, &quot;cam4&quot;:1.0, &quot;cam5&quot;:1.0, &quot;set1&quot;:1.5, &quot;set2&quot;:1.8, &quot;set3&quot;:1.5},
                        help=&#39;{&quot;cam0&quot;: 1.0, &quot;set1&quot;: 2.0}&#39;)
    # 클래스 가중치 조절 (1.0이 기본 가중치로 픽셀 수에 따라서 조절.)
    parser.add_argument(&quot;--class_weights&quot;, type=arg_as_list, default=[2.0166, 3.481, 4.0911, 3.9912, 3.9619, 2.0864, 1.8396, 4.3168, 3.79, 6.4674, 5.7661, 5.642, 8.4116, 5.9525, 2.2137, 5.2137, 6.1661, 4.195, 1.0],
                        help=&#39;List of weights for each class. &quot;[1.0, 1.5, 0.8]&quot;&#39;)

    parser.add_argument(&quot;--use_ohem&quot;, action=&#39;store_true&#39;, help=&quot;Use OHEM Cross Entropy loss&quot;)

    args = parser.parse_args()

    result_dir = Path(args.result_dir)
    result_dir.mkdir(parents=True, exist_ok=True)

    train_and_validate(args)


# 학습 시작 명령어 기본.
python train.py \
    --dataset_dir &quot;./data&quot; \
    --result_dir &quot;./output_001&quot; \
    --loadpath &quot;./DDRNet23s_cityscape.pth&quot; \
    --epochs 300 \
    --batch_size 16 \
    --lr 1e-2 \
    --folder_weights &#39;{&quot;cam0&quot;:1.0, &quot;cam1&quot;:1.0, &quot;cam2&quot;:1.0, &quot;cam3&quot;:1.0, &quot;cam4&quot;:1.0, &quot;cam5&quot;:1.0, &quot;set1&quot;:1.5, &quot;set2&quot;:1.5, &quot;set3&quot;:1.5}&#39; \
    --class_weights &#39;[0.5, 1.0, 1.0, 1.0, 1.0, 1.5, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]&#39; \
    --use_ohem
</code></pre>
<h3 id="학습-실행-최종-명령어">학습 실행 최종 명령어.</h3>
<blockquote>
<p>python train_weight.py --loadpath ./output/model_best.pth --epoch 300  --batch_size 16</p>
</blockquote>
<p>학습 loss값이 일정해지는 수준까지 학습 진행을 예측하고 300Epoch까지 진행.
추가 학습을 진행하였으나 loss값이 정체되어 정지.</p>
<h3 id="학습-결과">학습 결과</h3>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/b2a3cad6-f053-4242-9210-18b6157cef89/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/b18e33f5-3334-48c8-b358-847f988de4ab/image.png" alt=""></p>
<p>학습이 중간에 끊겨서 다시 시작하는 과정에서 LR값이 초기화</p>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/28f2bdec-bd19-4cb4-ab14-f208ab250416/image.png" alt=""></p>
<p>학습이 끊기며 LR값이 초기화 되는 과정에서 잠시 loss값에 변동이 존재.</p>
<h4 id="test-dataset-predict-결과">test dataset predict 결과</h4>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/chan_woo_00/post/3ecd2d85-12a1-4070-8816-ecb6765ca610/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/chan_woo_00/post/20da27cb-a396-46f3-80a4-7d1848dd0749/image.png" alt=""></th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/chan_woo_00/post/b0a0c642-cf9d-4bf4-8364-0701472ce0c5/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/chan_woo_00/post/ac889142-b957-417d-ae55-123182a4e68b/image.png" alt=""></td>
</tr>
</tbody></table>
<h4 id="miou값-계산">mIoU값 계산.</h4>
<table>
<thead>
<tr>
<th><img src=https://velog.velcdn.com/images/chan_woo_00/post/5088da6b-5b80-48e1-b9e7-9c8abf81361a/image.png align="left"> 클래스가 없는 경우를 포함하였을 때</th>
<th><img src=https://velog.velcdn.com/images/chan_woo_00/post/fc41849e-ac44-4e2b-a410-370439a88410/image.png align="right"> 클래스가 없는 경우를 제외하였을 때</th>
</tr>
</thead>
</table>
<table>
<thead>
<tr>
<th align="left">클래스</th>
<th align="center">IoU</th>
<th align="center">증감</th>
</tr>
</thead>
<tbody><tr>
<td align="left">0(주행가능영역)</td>
<td align="center">0.6299</td>
<td align="center">▼</td>
</tr>
<tr>
<td align="left">1(인도)</td>
<td align="center">0.4093</td>
<td align="center">▼</td>
</tr>
<tr>
<td align="left">2(도로노면표시)</td>
<td align="center">0.2963</td>
<td align="center">▼</td>
</tr>
<tr>
<td align="left">3(차선)</td>
<td align="center">0.4273</td>
<td align="center">▼</td>
</tr>
<tr>
<td align="left">4(연석)</td>
<td align="center">0.3830</td>
<td align="center">◆</td>
</tr>
<tr>
<td align="left">5(벽,울타리)</td>
<td align="center">0.2677</td>
<td align="center">▲</td>
</tr>
<tr>
<td align="left">6(승용차)</td>
<td align="center">0.5337</td>
<td align="center">◆</td>
</tr>
<tr>
<td align="left">7(트럭)</td>
<td align="center">0.3242</td>
<td align="center">▲</td>
</tr>
<tr>
<td align="left">8(버스)</td>
<td align="center">0.5340</td>
<td align="center">▲</td>
</tr>
<tr>
<td align="left">9(바이크, 자전거)</td>
<td align="center">0.0747</td>
<td align="center">▲</td>
</tr>
<tr>
<td align="left">10(기타 차량)</td>
<td align="center">N/A</td>
<td align="center">◆</td>
</tr>
<tr>
<td align="left">11(보행자)</td>
<td align="center">0.3924</td>
<td align="center">▲</td>
</tr>
<tr>
<td align="left">12(라이더)</td>
<td align="center">N/A</td>
<td align="center">◆</td>
</tr>
<tr>
<td align="left">13(교통용 콘 및 봉)</td>
<td align="center">0.1015</td>
<td align="center">▲</td>
</tr>
<tr>
<td align="left">14(기타 수직 물체)</td>
<td align="center">0.6785</td>
<td align="center">◆</td>
</tr>
<tr>
<td align="left">15(건물)</td>
<td align="center">0.4426</td>
<td align="center">▲</td>
</tr>
<tr>
<td align="left">16(교통 표지)</td>
<td align="center">0.2713</td>
<td align="center">▲</td>
</tr>
<tr>
<td align="left">17(교통 신호)</td>
<td align="center">0.1287</td>
<td align="center">▲</td>
</tr>
<tr>
<td align="left">18 (기타)</td>
<td align="center">0.7875</td>
<td align="center">▼</td>
</tr>
</tbody></table>
<p>픽셀 수가 낮은 데이터들은 확실히 데이터가 증가함. (0.1 단위로 증가한 값들도 존재)
픽셀 수가 다수인 0, 2, 3, 18번 같은 경우에는 정확도가 감소하는 현상 발견.
클래스와 폴더의 가중치를 동시에 준 결과로 클래스와 폴더의 가중치 값들이 각각 어느정도 IoU값에 영향을 주는지 확인이 필요.</p>
<h4 id="inference-time">inference time</h4>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/c70fc1c7-3c64-4197-a558-b94d0aaeef33/image.png" alt="">
모델 구조에 대해서 현재 환경에 약 9.3ms소요</p>
<h3 id="결과-종합">결과 종합</h3>
<p>모델명: DDRNet</p>
<p>데이터셋 : 제공된 데이터셋 <code>7 : 2 : 1</code>로 분할시켜 학습 및 테스트 진행</p>
<pre><code class="language-python">import os
import shutil
from pathlib import Path

def split_dataset(base_dir):
    main_folders = [&#39;colormap&#39;, &#39;image&#39;, &#39;labelmap&#39;]

    source_base_path = Path(base_dir) / &#39;image&#39; / &#39;train&#39;

    if not source_base_path.exists():
        return

    # train 폴더 내의 하위 폴더들(cam0, cam1, set1 등) 목록 가져오기
    try:
        sub_folders = [d.name for d in source_base_path.iterdir() if d.is_dir()]
    except OSError as e:
        print(f&quot;문제가 발생했습니다&quot;)
        return

    print(&quot;데이터셋 분할을 시작&quot;)

    for sub_folder in sub_folders:
        print(f&quot;\n📁 [{sub_folder}] 폴더 처리 중...&quot;)

        source_sub_folder_path = source_base_path / sub_folder

        try:
            files = sorted([f.name for f in source_sub_folder_path.iterdir() if f.is_file()])
        except FileNotFoundError:
            print(f&quot;  &#39;{source_sub_folder_path}&#39; 폴더를 찾을 수 없습니다..&quot;)
            continue

        if not files:
            print(f&quot;  &#39;{sub_folder}&#39; 폴더에 파일이 없습니다.&quot;)
            continue

        for main_folder in main_folders:
            for split_type in [&#39;train&#39;, &#39;val&#39;, &#39;test&#39;]:
                dest_path = Path(base_dir) / main_folder / split_type / sub_folder
                dest_path.mkdir(parents=True, exist_ok=True)

        moved_counts = {&#39;train&#39;: 0, &#39;val&#39;: 0, &#39;test&#39;: 0}
        for i in range(0, len(files), 10):
            chunk = files[i:i+10]

            # 10개 미만이면 train으로 이동
            if len(chunk) &lt; 10:
                split_map = {&#39;train&#39;: chunk}
            # 10개이면 7:2:1로 분할
            else:
                split_map = {
                    &#39;train&#39;: chunk[0:7],
                    &#39;val&#39;: chunk[7:9],
                    &#39;test&#39;: chunk[9:10]
                }
            for split_type, files_to_move in split_map.items():
                if not files_to_move:
                    continue

                for file_name in files_to_move:
                    moved_counts[split_type] += 1
                    for main_folder in main_folders:
                        source_file = Path(base_dir) / main_folder / &#39;train&#39; / sub_folder / file_name
                        dest_file = Path(base_dir) / main_folder / split_type / sub_folder / file_name

                        if source_file.exists():
                            shutil.move(str(source_file), str(dest_file))

        print(f&quot;  - ✅ Train: {moved_counts[&#39;train&#39;]}개 파일 이동 완료&quot;)
        print(f&quot;  - ✅ Validation: {moved_counts[&#39;val&#39;]}개 파일 이동 완료&quot;)
        print(f&quot;  - ✅ Test: {moved_counts[&#39;test&#39;]}개 파일 이동 완료&quot;)


if __name__ == &#39;__main__&#39;:
    base_directory = &#39;C:/etri/data&#39;  

    split_dataset(base_directory)</code></pre>
<p>테스트 코드</p>
<h4 id="prediction">prediction</h4>
<pre><code class="language-python">import os
import argparse
from glob import glob
from PIL import Image
import numpy as np
from tqdm import tqdm
import torch
import torch.nn.functional as F
from torchvision import transforms
from DDRNet import DDRNet
from torch.utils.data import Dataset, DataLoader
import matplotlib.cm as cm
from collections import OrderedDict


class TestSegmentationDataset(Dataset):
    def __init__(self, root_dir, subset=&#39;test&#39;):
        self.image_dir = os.path.join(root_dir, &quot;image&quot;, subset)
        self.image_paths = sorted(glob(os.path.join(self.image_dir, &quot;*&quot;, &quot;*.*&quot;), recursive=True))
        self.to_tensor = transforms.ToTensor()

    def __len__(self):
        return len(self.image_paths)

    def __getitem__(self, idx):
        img_path = self.image_paths[idx]
        img = Image.open(img_path).convert(&quot;RGB&quot;)
        tensor = self.to_tensor(img)
        return tensor, img_path

# 단일 GPU
def load_model(weight_path, num_classes, device):
    model = DDRNet(num_classes=num_classes)

    checkpoint = torch.load(weight_path, map_location=device)
    if &#39;model_state_dict&#39; in checkpoint:
        state_dict = checkpoint[&#39;model_state_dict&#39;]
    else:
        state_dict = checkpoint

    new_state_dict = OrderedDict()
    for k, v in state_dict.items():
        name = k[7:] if k.startswith(&#39;module.&#39;) else k # &#39;module.&#39; 접두사를 제거
        new_state_dict[name] = v

    model.load_state_dict(new_state_dict)

    model = model.to(device)
    model.eval()

    return model

# 예측 결과를 이미지 파일로 저장
def save_prediction(pred, save_path, colormap_root, num_classes):
    pred_np = pred.squeeze().cpu().numpy().astype(np.uint8)

    os.makedirs(os.path.dirname(save_path), exist_ok=True)
    Image.fromarray(pred_np).save(save_path)

    normed = pred_np.astype(np.float32) / (num_classes - 1) 
    cmap = cm.get_cmap(&#39;turbo&#39;)
    colored = cmap(normed)
    rgb = (colored[:, :, :3] * 255).astype(np.uint8)
    rgb_img = Image.fromarray(rgb)

    try:
        rel_path = os.path.relpath(save_path, start=os.path.dirname(save_path))
        cmap_path = os.path.join(colormap_root, os.path.dirname(os.path.relpath(save_path, start=args.result_dir)), rel_path)
    except ValueError: # 다른 드라이브에 있을 경우 대비
        rel_path = Path(save_path).name
        cmap_path = os.path.join(colormap_root, rel_path)

    os.makedirs(os.path.dirname(cmap_path), exist_ok=True)
    rgb_img.save(cmap_path)

# 전체 테스트
def test(args):
    # 단일 GPU 사용
    device = torch.device(&quot;cuda:0&quot; if torch.cuda.is_available() else &quot;cpu&quot;)
    print(f&quot;Using device: {device}&quot;)

    dataset = TestSegmentationDataset(args.dataset_dir, subset=args.subset)
    if not dataset.image_paths:
        print(f&quot;Error: No images found in &#39;{dataset.image_dir}&#39;. Please check the path and subset.&quot;)
        return

    dataloader = DataLoader(dataset, batch_size=1, shuffle=False, num_workers=2)

    model = load_model(args.weight_path, args.num_classes, device)
    colormap_root = os.path.join(args.result_dir, &quot;colormap&quot;)

    with torch.inference_mode():
        for img_tensor, img_path_tuple in tqdm(dataloader, desc=&quot;Predicting...&quot;):
            img_path = img_path_tuple[0]
            img_tensor = img_tensor.to(device)

            output = model(img_tensor)
            if isinstance(output, tuple):
                output = output[0]

            pred = torch.argmax(output, dim=1)

            rel_path = os.path.relpath(img_path, start=os.path.join(args.dataset_dir, &quot;image&quot;))
            save_path = os.path.join(args.result_dir, rel_path)

            save_prediction(pred, save_path, colormap_root, args.num_classes)

if __name__ == &quot;__main__&quot;:
    parser = argparse.ArgumentParser()
    parser.add_argument(&quot;--dataset_dir&quot;, type=str, default=&quot;./data&quot;, help=&quot;Path to dataset root directory&quot;)
    parser.add_argument(&quot;--weight_path&quot;, type=str, default=&quot;./output/model_best.pth&quot;, help=&quot;Path to model weight (.pth)&quot;)
    parser.add_argument(&quot;--result_dir&quot;, type=str, default=&quot;./result&quot;, help=&quot;Directory to save results&quot;)
    parser.add_argument(&quot;--num_classes&quot;, type=int, default=19, help=&quot;Number of segmentation classes&quot;)
    parser.add_argument(&quot;--subset&quot;, type=str, default=&quot;test&quot;, help=&quot;Which subset to run prediction on (e.g., &#39;test&#39;, &#39;val&#39;, &#39;train&#39;)&quot;)

    args = parser.parse_args()

    os.makedirs(args.result_dir, exist_ok=True)

    test(args)</code></pre>
<h4 id="miou-계산">mIoU 계산</h4>
<pre><code class="language-python">import os
import argparse
import numpy as np
from PIL import Image
from glob import glob
from tqdm import tqdm
from sklearn.metrics import confusion_matrix
from pathlib import Path

def load_image(path):
    return np.array(Image.open(path)).astype(np.uint8)

def compute_miou(confusion, num_classes):
    &quot;&quot;&quot;
    1. mIoU (All): NaN을 제외한 모든 클래스(IoU=0 포함)의 평균
    2. mIoU (&gt;0): IoU가 0보다 큰 클래스들만의 평균
    &quot;&quot;&quot;
    ious = []
    for cls in range(num_classes):
        TP = confusion[cls, cls]
        FP = confusion[:, cls].sum() - TP
        FN = confusion[cls, :].sum() - TP

        denom = TP + FP + FN
        if denom == 0:
            iou = float(&#39;nan&#39;)
        else:
            iou = TP / denom
        ious.append(iou)

    # mIoU (All Classes) 계산
    # NaN 값을 무시하고 평균을 계산.
    miou_all = np.nanmean(ious)

    # mIoU (IoU &gt; 0 Classes Only) 계산
    # IoU가 NaN이 아니고 0보다 큰 값들만 계산.
    positive_ious = [iou for iou in ious if not np.isnan(iou) and iou &gt; 0]

    # 0이 제거된 iou 값들의 평균을 계산
    if not positive_ious:
        miou_positive = 0.0
    else:
        miou_positive = np.mean(positive_ious) 

    return miou_all, miou_positive, ious

def evaluate(result_dir, label_dir, num_classes):
    pred_paths = sorted(glob(os.path.join(result_dir, &quot;**&quot;, &quot;*_leftImg8bit.png&quot;), recursive=True))
    print(f&#39;Found {len(pred_paths)} segmentation result images in {result_dir}&#39;)

    if not pred_paths:
        print(&quot;Error: No prediction files found. Please check the &#39;result_dir&#39; path and file names.&quot;)
        return

    all_confusion = np.zeros((num_classes, num_classes), dtype=np.int64)

    for pred_path in tqdm(pred_paths, desc=&quot;Evaluating&quot;):
        sub_folder = Path(pred_path).parent.name
        file_id = os.path.basename(pred_path).replace(&quot;_leftImg8bit.png&quot;, &quot;&quot;)

        label_path = os.path.join(label_dir, sub_folder, f&quot;{file_id}_gtFine_CategoryId.png&quot;)

        if not os.path.exists(label_path):
            print(f&quot;Label not found at {label_path}, skipping.&quot;)
            continue

        pred = load_image(pred_path).flatten()
        label = load_image(label_path).flatten()

        mask = label != 255
        pred = pred[mask]
        label = label[mask]

        pred = np.clip(pred, 0, num_classes - 1)
        label = np.clip(label, 0, num_classes - 1)

        conf = confusion_matrix(label, pred, labels=list(range(num_classes)))
        all_confusion += conf

    miou_all, miou_positive, ious = compute_miou(all_confusion, num_classes)

    print(&quot;\n--- Evaluation Results ---&quot;)
    print(f&quot;📊 mIoU (All Classes, IoU=0 포함): {miou_all:.4f}&quot;)
    print(f&quot;📊 mIoU (Positive Classes, IoU&gt;0 제외): {miou_positive:.4f}&quot;)
    print(&quot;--------------------------&quot;)

    for i, iou in enumerate(ious):
        print(f&quot;Class {i}: IoU = {iou:.4f}&quot; if not np.isnan(iou) else f&quot;Class {i}: IoU = NaN (ignored in mean)&quot;)

if __name__ == &quot;__main__&quot;:
    parser = argparse.ArgumentParser(description=&quot;Calculate mIoU for semantic segmentation results.&quot;)

    parser.add_argument(&quot;--result_dir&quot;, type=str, default=&quot;C:/ETRI/result/test&quot;, 
                        help=&quot;Predicted *_leftImg8bit.png files가 있는 상위 디렉토리&quot;)
    parser.add_argument(&quot;--label_dir&quot;, type=str, default=&quot;C:/ETRI/data/labelmap/test&quot;, 
                        help=&quot;정답 레이블 *_gtFine_CategoryId.png files가 있는 상위 디렉토리&quot;)
    parser.add_argument(&quot;--num_classes&quot;, type=int, default=19, help=&quot;세그먼테이션 클래스 수&quot;)

    args = parser.parse_args()

    evaluate(args.result_dir, args.label_dir, args.num_classes)</code></pre>
<img src="https://velog.velcdn.com/images/chan_woo_00/post/145fee9f-8eca-419b-8ce0-b6fedc76b381/image.png">

<blockquote>
<p>개선방법 1 : <strong>폴더별 가중치 조절</strong>
개선방법 2 : <strong>클래스별 가중치 조절</strong>
개선방법 3 : <strong>데이터 증식 추가</strong></p>
</blockquote>
<table>
<thead>
<tr>
<th></th>
<th align="center">baseline</th>
<th align="center">DDRNet_weight</th>
</tr>
</thead>
<tbody><tr>
<td>mIoU</td>
<td align="center">0.3228</td>
<td align="center">0.3528</td>
</tr>
<tr>
<td>inference time</td>
<td align="center">9.375ms</td>
<td align="center">9.375ms</td>
</tr>
</tbody></table>
<h3 id="ddrnet에-대한-개선-방향">DDRNet에 대한 개선 방향</h3>
<ul>
<li>데이터 증식 방법 추가</li>
<li>폴더에 가중치를 주는 방향과 클래스별로 가중치를 주는 방향에 대해 효율적인 방향 탐구 필요.</li>
<li>Train dataset(set1) 중 일부 데이터가 차량 본넷의 클래스가 잘못 라벨링이 되어있는것을 확인, 해당 데이터를 6번(차량) 클래스에서 18번(기타) 클래스로 변경
<img src="https://velog.velcdn.com/images/chan_woo_00/post/be194894-3a37-492f-829d-fc89fa86eb26/image.png" alt=""></li>
<li>모델 구조에서 반복된 연산으로 인해서 연산량은 늘어나나 효율은 낮은 구조에 대해 개선.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[DDRNet 학습.]]></title>
            <link>https://velog.io/@chan_woo_00/DDRNet-%ED%95%99%EC%8A%B5</link>
            <guid>https://velog.io/@chan_woo_00/DDRNet-%ED%95%99%EC%8A%B5</guid>
            <pubDate>Mon, 22 Sep 2025 06:43:56 GMT</pubDate>
            <description><![CDATA[<h2 id="ddrnet-코드-학습">DDRNet 코드 학습</h2>
<h3 id="환경-구성">환경 구성.</h3>
<p>중요 환경에 대한 버전.</p>
<pre><code>Package             Version
------------------- --------------------
numpy               1.24.1
opencv-python       4.12.0.88
thop                0.1.1.post2209072238
torch               2.3.1+cu118
torchaudio          2.3.1+cu118
torchvision         0.18.1+cu118</code></pre><p>ㅇㅇ
####
진행 작업</p>
<blockquote>
<ol>
<li>BaseLine code Train 코드 개선 및 수행.
학습된 Epoch까지의 데이터에 대해서 추론 및 mIoU 계산.</li>
<li>Backbone을 freeze하여 Backbone의 가중치를 그대로 가진 상태로 추론 작업을 수행 가능하도록 개선.</li>
<li>다중 GPU작업에 맞춰진 환경을 단일 GPU 환경으로 개선.</li>
<li>DataLoader나 학습 파라미터 등의 인자를 parser로 받아 조절 가능하게 개선.</li>
<li>학습이 길어지는 경우(중간에 끊어야 하는 경우)를 대비해서 CheckPoint model을 받아 학습을 이어 받을 수 있도록 개선</li>
<li>데이터셋의 일부(20%)를 Validation작업에 수행하기 위해서 데이터를 이동.</li>
<li>train에 대해서 eval의 과정이 없는 코드에 eval DataLoader를 사용하여 train 중간에 eval과정을 거치도록 개선.</li>
</ol>
</blockquote>
<h4 id="trainpy">train.py</h4>
<pre><code class="language-python">import os
import argparse
import torch
from torch.utils.data import DataLoader
from tqdm import tqdm
from collections import OrderedDict
from DDRNet import DDRNet
from functions import *
from pathlib import Path

def train_and_validate(args):
    device = torch.device(&quot;cuda:0&quot; if torch.cuda.is_available() else &quot;cpu&quot;)
    print(f&quot;Initialized single GPU training on device: {device}&quot;)

    train_dataset = SegmentationDataset(args.dataset_dir, args.crop_size, &#39;train&#39;, args.scale_range)
    val_dataset = SegmentationDataset(args.dataset_dir, args.crop_size, &#39;val&#39;, args.scale_range)

    print(f&quot;DataLoader settings: num_workers={args.num_workers}, pin_memory={args.pin_memory}, shuffle={args.shuffle}, drop_last={args.drop_last}&quot;)
    train_dataloader = DataLoader(train_dataset, batch_size=args.batch_size, shuffle=args.shuffle, num_workers=args.num_workers, pin_memory=args.pin_memory, drop_last=args.drop_last)
    val_dataloader = DataLoader(val_dataset, batch_size=args.batch_size, shuffle=False, num_workers=args.num_workers, pin_memory=args.pin_memory)

    model = DDRNet(num_classes=args.num_classes).to(device)

    criterion = CrossEntropy(ignore_label=255)

    if args.freeze_backbone:
        print(&quot;❄️ Freezing backbone layers...&quot;)
        backbone_layer_names = [&#39;conv1&#39;, &#39;layer1&#39;, &#39;layer2&#39;, &#39;layer3&#39;, &#39;layer4&#39;, &#39;spp&#39;] 
        for name, param in model.named_parameters():
            if any(name.startswith(layer_name) for layer_name in backbone_layer_names):
                param.requires_grad = False

    params_to_update = [p for p in model.parameters() if p.requires_grad]
    print(f&quot;Total parameters: {len(list(model.parameters()))}, Trainable parameters: {len(params_to_update)}&quot;)

    optimizer = torch.optim.SGD(params_to_update, lr=args.lr, momentum=args.momentum, weight_decay=args.weight_decay)
    scheduler = WarmupPolyEpochLR(optimizer, total_epochs=args.epochs, warmup_epochs=args.warmup_epochs, warmup_ratio=5e-4)

    start_epoch = 0
    min_val_loss = float(&#39;inf&#39;)
    if args.loadpath is not None:
        print(f&quot;Loading checkpoint from: {args.loadpath}&quot;)
        checkpoint = torch.load(args.loadpath, map_location=device)
        try:
            new_state_dict = OrderedDict()
            for k, v in checkpoint[&#39;model_state_dict&#39;].items():
                name = k[7:] if k.startswith(&#39;module.&#39;) else k
                new_state_dict[name] = v
            model.load_state_dict(new_state_dict, strict=False)

            optimizer.load_state_dict(checkpoint[&#39;optimizer_state_dict&#39;])
            scheduler.load_state_dict(checkpoint[&#39;scheduler_state_dict&#39;])
            start_epoch = checkpoint[&#39;epoch&#39;] + 1
            min_val_loss = checkpoint.get(&#39;loss&#39;, float(&#39;inf&#39;))
            print(f&quot;Resuming training from epoch {start_epoch}, with min_val_loss: {min_val_loss:.4f}&quot;)
        except KeyError:
            print(&quot;Old checkpoint format. Loading model state_dict only.&quot;)
            new_state_dict = OrderedDict()
            for k, v in checkpoint.items():
                if k.startswith(&#39;module.&#39;): name = k[7:]
                elif k.startswith(&#39;model.&#39;): name = k[6:]
                else: name = k
                new_state_dict[name] = v
            model.load_state_dict(new_state_dict, strict=False)

    os.makedirs(args.result_dir, exist_ok=True)
    log_path = os.path.join(args.result_dir, &quot;log.txt&quot;)
    mode = &#39;a&#39; if start_epoch &gt; 0 else &#39;w&#39;
    with open(log_path, mode) as f:
        if start_epoch == 0: f.write(&quot;Epoch\t\tTrain-loss\t\tVal-loss\t\tlearningRate\n&quot;)

    for epoch in range(start_epoch, args.epochs):
        model.train()
        total_train_loss = 0.0
        loop = tqdm(train_dataloader, desc=f&quot;Train [{epoch+1}/{args.epochs}]&quot;, ncols=100)

        for i, (imgs, labels) in enumerate(loop):
            optimizer.zero_grad(set_to_none=True)
            imgs, labels = imgs.to(device), labels.to(device)
            outputs = model(imgs)
            loss = criterion(outputs, labels)
            loss.backward()
            optimizer.step()
            total_train_loss += loss.item()
            loop.set_postfix(loss=loss.item(), avg_loss=total_train_loss/(i+1), lr=scheduler.get_last_lr()[0])

        avg_train_loss = total_train_loss / len(train_dataloader)
        scheduler.step()

        avg_val_loss_str = &quot;N/A&quot;
        if (epoch + 1) % 5 == 0 or (epoch + 1) == args.epochs:
            model.eval()
            total_val_loss = 0.0
            with torch.no_grad():
                loop_val = tqdm(val_dataloader, desc=f&quot;Val [{epoch+1}/{args.epochs}]&quot;, ncols=100)
                for i, (imgs, labels) in enumerate(loop_val):
                    imgs, labels = imgs.to(device), labels.to(device)
                    outputs = model(imgs)
                    loss = criterion(outputs, labels)
                    total_val_loss += loss.item()

            avg_val_loss = total_val_loss / len(val_dataloader)
            avg_val_loss_str = f&quot;{avg_val_loss:.4f}&quot;

            print(f&quot;\nEpoch {epoch+1}: Train Loss = {avg_train_loss:.4f}, Validation Loss = {avg_val_loss:.4f}&quot;)

            if avg_val_loss &lt; min_val_loss:
                min_val_loss = avg_val_loss
                ckp_path = os.path.join(args.result_dir, &quot;model_best.pth&quot;)
                state_to_save = {
                    &#39;epoch&#39;: epoch, &#39;model_state_dict&#39;: model.state_dict(),
                    &#39;optimizer_state_dict&#39;: optimizer.state_dict(), &#39;scheduler_state_dict&#39;: scheduler.state_dict(),
                    &#39;loss&#39;: min_val_loss,
                }
                torch.save(state_to_save, ckp_path)
                print(f&quot;Best model saved at epoch {epoch+1} with val loss {min_val_loss:.4f}&quot;)

            ckp_path = os.path.join(args.result_dir, f&quot;model_epoch{epoch+1}.pth&quot;)
            state_to_save = {
                &#39;epoch&#39;: epoch, &#39;model_state_dict&#39;: model.state_dict(),
                &#39;optimizer_state_dict&#39;: optimizer.state_dict(), &#39;scheduler_state_dict&#39;: scheduler.state_dict(),
                &#39;loss&#39;: avg_val_loss,
            }
            torch.save(state_to_save, ckp_path)

        lr = scheduler.get_last_lr()[0]
        with open(log_path, &quot;a&quot;) as f:
            log_entry = f&quot;\n{epoch + 1}\t\t{avg_train_loss:.4f}\t\t{avg_val_loss_str}\t\t{lr:.8f}&quot;
            f.write(log_entry)

if __name__ == &quot;__main__&quot;:
    parser = argparse.ArgumentParser(description=&quot;DDRNet Training Script&quot;)

    parser.add_argument(&quot;--dataset_dir&quot;, type=str, default=&quot;./data&quot;, help=&quot;Path to dataset root&quot;)
    parser.add_argument(&quot;--loadpath&quot;, type=str, default=None, help=&quot;Path to checkpoint for resuming training&quot;)
    parser.add_argument(&quot;--result_dir&quot;, type=str, default=&quot;output&quot;, help=&quot;Directory to save results&quot;)
    parser.add_argument(&quot;--epochs&quot;, type=int, default=400, help=&quot;Total number of training epochs&quot;)
    parser.add_argument(&quot;--num_classes&quot;, type=int, default=19, help=&quot;Number of segmentation classes&quot;)

    parser.add_argument(&quot;--lr&quot;, type=float, default=1e-2, help=&quot;Initial learning rate&quot;)
    parser.add_argument(&quot;--batch_size&quot;, type=int, default=8, help=&quot;Training batch size&quot;)
    parser.add_argument(&quot;--momentum&quot;, type=float, default=0.9, help=&quot;Momentum for SGD optimizer&quot;)
    parser.add_argument(&quot;--weight_decay&quot;, type=float, default=5e-4, help=&quot;Weight decay for SGD optimizer&quot;)
    parser.add_argument(&quot;--warmup_epochs&quot;, type=int, default=5, help=&quot;Number of warmup epochs for scheduler&quot;)

    parser.add_argument(&quot;--crop_size&quot;, default=[512, 1024], type=arg_as_list, help=&quot;Crop size (H W)&quot;)
    parser.add_argument(&quot;--scale_range&quot;, default=[0.75, 1.5], type=arg_as_list, help=&quot;Resize input scale range&quot;)

    parser.add_argument(&quot;--num_workers&quot;, type=int, default=os.cpu_count(), help=&quot;Number of workers for DataLoader&quot;)
    parser.add_argument(&quot;--no_pin_memory&quot;, action=&quot;store_false&quot;, dest=&quot;pin_memory&quot;, help=&quot;Disable pin_memory for DataLoader&quot;)
    parser.add_argument(&quot;--no_shuffle&quot;, action=&quot;store_false&quot;, dest=&quot;shuffle&quot;, help=&quot;Disable shuffling for training data&quot;)
    parser.add_argument(&quot;--no_drop_last&quot;, action=&quot;store_false&quot;, dest=&quot;drop_last&quot;, help=&quot;Disable drop_last for training data&quot;)
    parser.set_defaults(pin_memory=True, shuffle=True, drop_last=True)

    parser.add_argument(&quot;--freeze_backbone&quot;, action=&#39;store_true&#39;, help=&quot;Freeze backbone layers for fine-tuning&quot;)

    args = parser.parse_args()

    result_dir = Path(args.result_dir)
    result_dir.mkdir(parents=True, exist_ok=True)

    train_and_validate(args)</code></pre>
<p>DDRNet23s_imagenet.pth파일의 가중치를 받아서 진행.
Backbone을 freeze하고 학습을 진행하는 경우 전체 파라미터의 1/3정도만 학습이 되고 학습 진행에 있어서 Train의 Loss값이 너무 느리게 학습되는 현상이 발견되어 Backbone freeze작업을 수행하지 않고 전체적으로 모두 수행하기로 함.
Backbone을 imagenet과 cityscape로 모두 학습을 수행했으나 유의미한 차이를 발견하지 못함.
<img src="https://velog.velcdn.com/images/chan_woo_00/post/4e0644f0-d739-4637-8f98-362a074beb0a/image.png" alt=""></p>
<h4 id="학습-실행-최종-명령어">학습 실행 최종 명령어.</h4>
<blockquote>
<p>python backbone_freeze_train.py --loadpath ./DDRNet_cityscape.pth --batch_size 16</p>
</blockquote>
<p>학습 Epoch은 200Epoch으로 진행하였을 때 계속해서 Loss값이 낮아지는 경향이 있어 CheckPoint로 이어서 학습하기로 하고 크게 500으로 설정.</p>
<h4 id="학습-결과">학습 결과</h4>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/74927cdc-9cfb-4b67-93bb-8363d832d80a/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/dc37ffb1-ee43-4454-a6b2-d00fef2eca33/image.png" alt="LR"></p>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/bfbf4f71-536d-48e8-b820-f08921cbc458/image.png" alt="">
낮게나마 낮아지던 loss값이 300Epoch가까이 진행되었을 때 무의미하다고 판단하여 학습을 종료</p>
<h4 id="test-dataset-predict-결과-200epoch에-대한-추론">test dataset predict 결과 (200Epoch에 대한 추론)</h4>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/chan_woo_00/post/6abfe8f1-964e-4725-84c6-fecdda98d6bd/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/chan_woo_00/post/a8020b6a-9af4-4c58-9991-17197b689b5e/image.png" alt=""></th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/chan_woo_00/post/2534ac74-d858-4de0-84b1-cf163557c2ee/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/chan_woo_00/post/35b6002f-1f81-42f2-9ba3-8740219d3556/image.png" alt=""></td>
</tr>
</tbody></table>
<h4 id="miou값-계산">mIoU값 계산.</h4>
<table>
<thead>
<tr>
<th><img src=https://velog.velcdn.com/images/chan_woo_00/post/5088da6b-5b80-48e1-b9e7-9c8abf81361a/image.png align="left"> 클래스가 없는 경우를 포함하였을 때</th>
<th><img src=https://velog.velcdn.com/images/chan_woo_00/post/fc41849e-ac44-4e2b-a410-370439a88410/image.png align="right"> 클래스가 없는 경우를 제외하였을 때</th>
</tr>
</thead>
<tbody><tr>
<td></td>
<td>0(주행가능영역)</td>
</tr>
<tr>
<td></td>
<td>1(인도)</td>
</tr>
<tr>
<td></td>
<td>2(도로노면표시)</td>
</tr>
<tr>
<td></td>
<td>3(차선)</td>
</tr>
<tr>
<td></td>
<td>4(연석)</td>
</tr>
<tr>
<td></td>
<td>5(벽,울타리)</td>
</tr>
<tr>
<td></td>
<td>6(승용차)</td>
</tr>
<tr>
<td></td>
<td>7(트럭)</td>
</tr>
<tr>
<td></td>
<td>8(버스)</td>
</tr>
<tr>
<td></td>
<td>9(바이크, 자전거)</td>
</tr>
<tr>
<td></td>
<td>10(기타 차량)</td>
</tr>
<tr>
<td></td>
<td>11(보행자)</td>
</tr>
<tr>
<td></td>
<td>12(라이더)</td>
</tr>
<tr>
<td></td>
<td>13(교통용 콘 및 봉)</td>
</tr>
<tr>
<td></td>
<td>14(기타 수직 물체)</td>
</tr>
<tr>
<td></td>
<td>15(건물)</td>
</tr>
<tr>
<td></td>
<td>16(교통 표지)</td>
</tr>
<tr>
<td></td>
<td>17(교통 신호)</td>
</tr>
<tr>
<td></td>
<td>18 (기타)</td>
</tr>
</tbody></table>
<ul>
<li>차량 객체 중 트럭의 가중치 낮음.</li>
<li>바이크, 기타 차량, 라이더 등에 대해 test로 넣은 이미지에 없는 지 0으로 mIoU결과값 추론</li>
<li>교통용 콘, 봉, 교통 표지, 교통 신호 등 작은 객체에 대한 정확도 낮음.</li>
<li>loss값에 비교하여 mIoU값이 불안정. 18번 클래스 기타에 대해 loss값이 맞춰진 것으로 추측</li>
</ul>
<h4 id="inference-time">inference time</h4>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/c70fc1c7-3c64-4197-a558-b94d0aaeef33/image.png" alt="">
모델 구조에 대해서 현재 환경에 약 9.3ms</p>
<h4 id="ddrnet에-대한-개선-방향">DDRNet에 대한 개선 방향</h4>
<ul>
<li>폴더와 클래스별로 가중치를 다르게 개선</li>
<li>폴더별로 클래스 분포를 확인</li>
<li>증강 기법 추가(우천, 강한 강원 등에 대비) - 색상에 제한되는 객체(신호등의 경우 Red, Blue, Green)를 신경쓸 필요가 없으니 색상 변환 등도 추가할 예정.</li>
<li>inference time을 개선할 방법 모색</li>
</ul>
<h4 id="모델-비교">모델 비교</h4>
<table>
<thead>
<tr>
<th>Model</th>
<th align="center">DDRNet</th>
<th align="center">Deeplabv3</th>
<th align="center">YOLOv11_m</th>
</tr>
</thead>
<tbody><tr>
<td>inference time</td>
<td align="center">9.3ms</td>
<td align="center">X</td>
<td align="center">7.3ms</td>
</tr>
<tr>
<td>mIoU</td>
<td align="center">0.3228</td>
<td align="center">0.6</td>
<td align="center">X</td>
</tr>
<tr>
<td>loss</td>
<td align="center">0.355</td>
<td align="center">0.18</td>
<td align="center">1.55</td>
</tr>
</tbody></table>
<p>mIoU값에 대해서는 Deeplabv3가 가장 높게 나오는 중이나 모델의 크기와 추론 시간에 대한 정확한 정보 필요.
YOLOv11_m은 YOLOv11_s 학습 결과가 나온 이후에 해당 값에 대해서 비교.
추론 시간이 모델의 사이즈가 낮아짐에 따라서 추론 시간에 장점이 있을 것으로 보임.
DDRNet은 추론 시간과 mIoU 값 등 여러 개선이 필요.</p>
<p>주의 요소 : 추론 시간 계산 시 장비에 따라 차이가 있음을 주의
(RTX 4070 super, RTX 5070(컴퓨터실))</p>
<p>Deeplabv3는 학습 진행 중이므로 설정한 Epoch이 진행된 모델 기준으로 mIoU값과 loss값 다시 정리 예정.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DDRNet custom dataset학습 시도.]]></title>
            <link>https://velog.io/@chan_woo_00/DDRNet-custom-dataset</link>
            <guid>https://velog.io/@chan_woo_00/DDRNet-custom-dataset</guid>
            <pubDate>Wed, 03 Sep 2025 05:45:17 GMT</pubDate>
            <description><![CDATA[<p><a href="https://github.com/ydhongHIT/DDRNet">https://github.com/ydhongHIT/DDRNet</a>
DDRNet연동 깃허브</p>
<p><a href="https://github.com/chenjun2hao/DDRNet.pytorch">https://github.com/chenjun2hao/DDRNet.pytorch</a>
해당 깃허브의 코드로 custom dataset 학습 시도</p>
<p>ETRI dataset에서 다운받은 format은 images -&gt; jpg와 labels -&gt; txt형태로 구성.</p>
<p>해당 깃허브에서는 labels의 형태를 mask를 받아 학습하는 형태로 구성.
(data loader에 따라서 mask를 그대로 사용하기도 하고 txt형태의 데이터를 받기도 한다)</p>
<p>label을 불러오는데에는 lst파일을 이용하여 경로를 받는다.</p>
<pre><code>1. txt를 png형태로 데이터셋을 변형

2. 해당 경로에 따른 lst파일 생성

3. 파일 변환이 정상적으로 되었는지 테스트.</code></pre><ol>
<li>txt2png 코드<pre><code class="language-python">import os
import cv2
import numpy as np
</code></pre>
</li>
</ol>
<p>#IMAGE_DIR = r&quot;C:\DDRNet\data\ETRI\train\images&quot;
#IMAGE_DIR = r&quot;C:\DDRNet\data\ETRI\val\images&quot;
IMAGE_DIR = r&quot;C:\DDRNet\data\ETRI\test\images&quot;</p>
<p>#TXT_LABEL_DIR = r&quot;C:\DDRNet\data\ETRI\train\labels&quot;
#TXT_LABEL_DIR = r&quot;C:\DDRNet\data\ETRI\val\labels&quot;
TXT_LABEL_DIR = r&quot;C:\DDRNet\data\ETRI\test\labels&quot;</p>
<p>#OUTPUT_PNG_DIR = r&quot;C:\DDRNet\data\ETRI\train\masks_png&quot;
#OUTPUT_PNG_DIR = r&quot;C:\DDRNet\data\ETRI\val\masks_png&quot;
OUTPUT_PNG_DIR = r&quot;C:\DDRNet\data\ETRI\test\masks_png&quot;</p>
<p>CLASS_TO_ID = {str(i): i for i in range(42)}</p>
<p>def convert_txt_to_png():
    os.makedirs(OUTPUT_PNG_DIR, exist_ok=True)</p>
<pre><code>txt_files = [f for f in os.listdir(TXT_LABEL_DIR) if f.endswith(&#39;.txt&#39;)]
print(f&quot;{len(txt_files)}개의 .txt 파일을 변환.&quot;)

processed_count = 0
for txt_filename in txt_files:
    base_filename = os.path.splitext(txt_filename)[0]

    img_path = None
    for ext in [&#39;.jpg&#39;, &#39;.jpeg&#39;, &#39;.png&#39;]:
        potential_path = os.path.join(IMAGE_DIR, base_filename + ext)
        if os.path.exists(potential_path):
            img_path = potential_path
            break

    if not img_path:
        print(f&quot;원본 이미지가 존재하지 않음.&quot;)
        continue

    img = cv2.imread(img_path)
    if img is None:
        print(f&quot;경로 오류&quot;)
        continue
    height, width, _ = img.shape

    mask = np.zeros((height, width), dtype=np.uint8)

    txt_path = os.path.join(TXT_LABEL_DIR, txt_filename)
    with open(txt_path, &#39;r&#39;) as f:
        for line in f.readlines():
            parts = line.strip().split()
            if len(parts) &lt; 3:
                continue

            class_id_str = parts[0]

            if class_id_str not in CLASS_TO_ID:
                continue
            mask_value = CLASS_TO_ID[class_id_str]

            if len(parts[1:]) % 2 != 0:
                continue

            try:
                normalized_coords = np.array(parts[1:], dtype=np.float32).reshape((-1, 2))

                pixel_coords = (normalized_coords * np.array([width, height])).astype(np.int32)

            except ValueError:
                continue

            cv2.fillPoly(mask, [pixel_coords], color=mask_value)

    output_path = os.path.join(OUTPUT_PNG_DIR, base_filename + &#39;.png&#39;)
    cv2.imwrite(output_path, mask)
    processed_count += 1
    if processed_count % 100 == 0:
        print(f&quot;{processed_count}/{len(txt_files)} 파일 처리 완료...&quot;)

print(f&quot;변환 완료&quot;)</code></pre><p>if <strong>name</strong> == &#39;<strong>main</strong>&#39;:
    convert_txt_to_png()</p>
<pre><code>

2. lst 파일 생성
```python
import os

def create_lst_files(base_path):
    splits = [&#39;train&#39;, &#39;val&#39;, &#39;test&#39;]

    for split in splits:
        # 예: C:\DDRNet\data\ETRI\train\images
        image_dir = os.path.join(base_path, split, &#39;images&#39;)

        if not os.path.isdir(image_dir):
            print(f&quot;폴더의 위치 찾을 수 없음.&quot;)
            continue

        lst_content = []

        image_files = os.listdir(image_dir)

        jpg_files = sorted([f for f in image_files if f.lower().endswith(&#39;.jpg&#39;)])

        for image_file in jpg_files:
            base_name = os.path.splitext(image_file)[0]

            image_path_relative = f&quot;{split}/images/{base_name}.jpg&quot;
            label_path_relative = f&quot;{split}/labels/{base_name}.txt&quot;

            line = f&quot;{image_path_relative} {label_path_relative}&quot;
            lst_content.append(line)

        if lst_content:
            lst_file_path = os.path.join(base_path, f&quot;{split}.lst&quot;)
            with open(lst_file_path, &#39;w&#39;) as f:
                f.write(&#39;\n&#39;.join(lst_content))
            print(f&quot;✅ &#39;{lst_file_path}&#39; (총 {len(lst_content)} 줄)&quot;)

dataset_base_path = r&#39;C:\DDRNet\data\ETRI&#39;
create_lst_files(dataset_base_path)</code></pre><ol start="3">
<li>파일 변환이 정상적으로 되었는지 테스트.<pre><code class="language-python">import os
import cv2
import numpy as np
from PIL import Image
</code></pre>
</li>
</ol>
<p>PNG_MASK_DIR = r&quot;C:\DDRNet\data\ETRI\train\masks_png&quot; 
OUTPUT_VIS_DIR = r&quot;C:\DDRNet\data\ETRI\train\masks_visualized&quot;</p>
<h1 id="dataset이-총-42종이므로-클래스를-41까지-색상을-분리해서-확인">dataset이 총 42종이므로 클래스를 41까지 색상을 분리해서 확인</h1>
<p>COLOR_PALETTE = [
    (0, 0, 0),       # 0: 배경 (Black)
    (128, 0, 0),     # 1: Dark Red
    (0, 128, 0),     # 2: Dark Green
    (128, 128, 0),   # 3: Dark Yellow
    (0, 0, 128),     # 4: Dark Blue
    (128, 0, 128),   # 5: Dark Magenta
    (0, 128, 128),   # 6: Dark Cyan
    (128, 128, 128), # 7: Gray
    (64, 0, 0),      # 8:
    (192, 0, 0),     # 9:
    (64, 128, 0),    # 10:
    (192, 128, 0),   # 11:
    (64, 0, 128),    # 12:
    (192, 0, 128),   # 13:
    (64, 128, 128),  # 14:
    (192, 128, 128), # 15:
    (0, 64, 0),      # 16:
    (128, 64, 0),    # 17:
    (0, 192, 0),     # 18:
    (128, 192, 0),   # 19:
    (0, 64, 128),    # 20:
    (128, 64, 128),  # 21:
    (0, 192, 128),   # 22:
    (128, 192, 128), # 23:
    (64, 64, 0),     # 24:
    (192, 64, 0),    # 25:
    (64, 192, 0),    # 26:
    (192, 192, 0),   # 27:
    (64, 64, 128),   # 28:
    (192, 64, 128),  # 29:
    (64, 192, 128),  # 30:
    (192, 192, 128), # 31:
    (0, 0, 64),      # 32:
    (128, 0, 64),    # 33:
    (0, 128, 64),    # 34:
    (128, 128, 64),  # 35:
    (0, 0, 192),     # 36:
    (128, 0, 192),   # 37:
    (0, 128, 192),   # 38:
    (128, 128, 192), # 39:
    (64, 0, 64),     # 40:
    (192, 0, 64),    # 41:
]</p>
<p>def visualize_masks():
    os.makedirs(OUTPUT_VIS_DIR, exist_ok=True)</p>
<pre><code>png_files = [f for f in os.listdir(PNG_MASK_DIR) if f.endswith(&#39;.png&#39;)]
print(f&quot;총 {len(png_files)}개의 .png 마스크를 시각화합니다.&quot;)

processed_count = 0
for png_filename in png_files:
    png_path = os.path.join(PNG_MASK_DIR, png_filename)

    mask = cv2.imread(png_path, cv2.IMREAD_UNCHANGED)

    if mask is None:
        print(f&quot;경고: 마스크 파일 {png_path}를 읽을 수 없습니다. 건너뜁니다.&quot;)
        continue

    height, width = mask.shape
    colored_mask = np.zeros((height, width, 3), dtype=np.uint8)

    # 각 픽셀의 클래스 ID에 따라 색상 적용
    for class_id in range(len(COLOR_PALETTE)):
        indices = (mask == class_id)

        colored_mask[indices, 0] = COLOR_PALETTE[class_id][0] # Blue 채널
        colored_mask[indices, 1] = COLOR_PALETTE[class_id][1] # Green 채널
        colored_mask[indices, 2] = COLOR_PALETTE[class_id][2] # Red 채널

    output_path = os.path.join(OUTPUT_VIS_DIR, png_filename)
    cv2.imshow(&quot;visualize&quot;, colored_mask)
    cv2.waitKey()
    #cv2.imwrite(output_path, colored_mask)

    processed_count += 1
    if processed_count % 100 == 0:
        print(f&quot;{processed_count}/{len(png_files)} 파일 시각화 완료...&quot;)</code></pre><p>if <strong>name</strong> == &#39;<strong>main</strong>&#39;:
    visualize_masks()</p>
<p>```</p>
<p><a href="!https://youtu.be/tBp6nLnyBx4">작동 영상</a></p>
<p>데이터셋 조정을 마치고 학습을 진행한 결과.</p>
<p>명령어
<code>python tools/train.py --cfg experiments/cityscapes/ddrnet_39.yaml</code>
<a href="!https://youtu.be/1B3cEUWKKog">prompt 내용</a></p>
<p>loss값이 0으로 고정되고 Acc값도 소수점의 자리에 위치하는 것을 확인.</p>
<p>dataset에 255와 같은 mask 값이 존재하는지 데이터 결함 테스트도 진행했으나 dataset 자체에는 문제가 없다는 것을 확인.
numpy 버전에 대해서 문제가 있는것으로 추정되나 현재 GPU와 알맞은 pytorch 버전에 대해서 환경을 맞출 수 없기에 다른 github code를 사용하는 방법으로 회선하여 학습을 시도해보는 것을 목표로 진행하기로 함.</p>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/8f691b85-e771-4e14-be64-39dbdcedbda2/image.png" alt="">
학습 관련 기능이 들어있는 github
DDRNet.pytorch, deci.ai 시도
Segmentation-Pytorch 학습 시도 중</p>
<p>Cityscapes dataset이 아닌 custom dataset을 학습하기에 data loader와 pkl등의 코드 생성중</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DDRNet(Deep Dual-resolution Networks)]]></title>
            <link>https://velog.io/@chan_woo_00/DDRNet</link>
            <guid>https://velog.io/@chan_woo_00/DDRNet</guid>
            <pubDate>Sun, 24 Aug 2025 20:34:53 GMT</pubDate>
            <description><![CDATA[<p>Deep Dual-resolution Networks review</p>
<p>Segmentation 작업을 수행하는 모델들은 속도를 챙기기 위해 <a href="https://eehoeskrap.tistory.com/431">Dilated Convolution</a>등을 사용하는데 해당 Conv layer의 단점은 픽셀의 정보를 건너뛰어 수집하기 때문에 특정 패턴의 정보를 받아들이지 못하는 경우가 많다.</p>
<p><code>DDRNet</code>에서는 Dilated Convolution작업을 수행하지 않는것은 아니지만 정보의 보존을 위해 <code>Dual-resolution network</code>(이중 해상도 네트워크)를 제안한다.</p>
<p><code>DDRNet</code>은 하나의 trunk에서 시작하여 <code>high-resolution feature maps</code>과 다운샘플링을 거친 <code>rich sementic information maps</code>을 추출한다. 두 branch는 정보 융합을 위해 <code>bilateral connection</code>이 진행된다.</p>
<p>rich semantic information maps의 경우 이후 Segment Head에 들어가기 이전 DAPPM 모듈 내에서 multi-scale context information을 추출하고 융합하여 최종 특성맵을 완성시킨다.</p>
<blockquote>
<p>branch : 데이터 처리 경로.
trunk : 병렬 branch로 나뉘기 전 공통 부분.
bilateral connection : 양방향 연결
DAPPM(Deep Aggregation Pyramid Pooling Module) : </p>
</blockquote>
<h2 id="ddrnet-architecture">DDRNet Architecture</h2>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/8eb7675f-2fd3-4660-a70d-d2c70bf699d3/image.png" alt="Architecture"></p>
<blockquote>
<p>RB(Residual Basic Blocks) : Convolution Layer로써 특성 추출.
RBB(Bottleneck Block) : feature dimension을 확장하여 다양한 종류의 특징 생성.
Seg.Head : Segment Head(점선 Head와 실선 Head 구별)</p>
</blockquote>
<p><code>RB 1/32</code>의 상단에 <code>RB 1/8</code>에서 점선으로 <code>Seg. Head</code>가 존재하는데 해당 Head는 <code>Auxiliary Loss(보조 손실값)</code>으로 <code>DNN</code>이 역전파를 진행하는 과정에서 초반 레이어에 영향이 적게 미치는 문제를 해결하기 위하여 중간의 Loss값을 계산하여 이후에 합치는 과정을 거친다. 비교적 메인 <code>Seg. Head</code>보다는 간단한 구조로 제작되어있다.</p>
<h4 id="deep-supervision">Deep Supervision</h4>
<p>학습이 완료되고 난 이후에는 모델에 $$L_a$$는 포함되지 않는다.</p>
<p>$$L_f = L_n + α*L_a$$</p>
<blockquote>
<blockquote>
<p>$$L_f$$ : 최종 손실
$$L_n$$ : 일반 손실
$$α * L_a$$ : 보조 손실</p>
</blockquote>
</blockquote>
<p>논문에서는 α값을 0.4로 두어 가중치 값을 조정하였다.</p>
<h3 id="bilateral-fusion">bilateral fusion</h3>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/fd3970e9-cba1-4c49-9507-d8c1c318f11a/image.png" alt="bilateral fusion"></p>
<p>저해상도의 특징맵과 고해상도의 특징맵의 정보를 교환하는 네트워크로 좌측은 저 해상도의 branch에서 가져온 특성맵이고 우측은 고해상도의 branch에서 가져온 특성맵이다.</p>
<p><code>Low-resolution branch</code>는 3 x 3 convolution layer에 대해 256의 채널 수를 가지고 
<code>High-resolution brach</code>는 3 x 3 convolution layer에 대해 128의 채널 수를 가진다.
<code>Low-resolution branch</code>에서 <code>High-resolution brach</code>로 넘어가는 경우에는 1 x 1 convolution layer를 거치며 채널 수를 128로 줄이고 이후 UpSample과정을 통해 이미지 사이즈를 늘린다.
<code>High-resolution brach</code>에서 <code>Low-resolution branch</code>로 넘어가는 경우에는 3 x 3 Convolution layer를 Stride값을 2로 설정하여 채널 수를 256으로 늘리는 것과 동시에 사이즈를 줄인다.</p>
<p><code>Low-resolution branch</code>의 채널 수가 많은 이유는 낮은 해상도에 대해 더 깊은 수준의 특징을 가지고 싶어하기 때문이며 <code>High-resolution brach</code>가 높은 해상도에 대해서 특성은 추출하나 너무 연산량과 정확도에 대해 중간 지점을 맞추기 위함이다.</p>
<p>RB 1/8에서 RB 1/32로 넘어가는 경우는 UpSample을 x4와 x8을 하는 과정을 거치며
RB 1/32에서 RB 1/8로 넘어가는 경우는 stride 값을 4와 8로 설정하는 과정을 거친다.</p>
<h3 id="dappm-module">DAPPM Module</h3>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/0e434e1f-d6ab-41c5-bd1a-8336a483b8b8/image.png" alt="DAPPM"></p>
<ul>
<li><code>kernel = 5,  stride=2</code> : 1/128 size 특성맵, 이후 1 x 1 conv를 거치고 UpSample하며 채널 수 조정</li>
<li><code>kernel = 9,  stride=4</code> : 1/256 size 특성맵, 이후 1 x 1 conv를 거치고 UpSample하며 채널 수 조정</li>
<li><code>kernel = 17, stride=8</code> : 1/512 size 특성맵, 이후 1 x 1 conv를 거치고 UpSample하며 채널 수 조정</li>
<li><code>kernel = H * W</code> : (1, 1) size의 특성맵, 이후 1 x 1 conv를 거치고 UpSample하며 채널 수 조정</li>
</ul>
<p>각 UpSample을 통해 확장된 특성맵은 3 x 3 Conv layer를 거치며 부자연스러운 특성들을 정비한다.</p>
<blockquote>
<p>X : 1 / 64의 사이즈를 가진 특성맵
$$y_i$$ : 1 / 64의 사이즈를 가진 특성맵</p>
</blockquote>
<h4 id="y_i">$$y_i$$</h4>
<ul>
<li>$$i = 1$$</li>
<li>$$1 &lt; i &lt; n$$</li>
<li>$$i = n$$</li>
</ul>
<p>1번을 제외한 나머지는 UpSample과정을 거치고 난 후 이전 사이즈의 정보를 더한 이후 Conv(3 * 3)을 진행.</p>
<p>위 과정을 거치며 각기 다른 size의 정보를 작은 size부터 넓은 size의 정보로 누적시켜 사이즈간 정보를 정교하게 만든다.</p>
<blockquote>
<p>$$C(1 * 1)$$ : 1 x 1 Conv layer
$$C(3 * 3)$$ : 3 x 3 Conv layer
U : UpSample
P : Pooling payer, P(global)은 Kernel = H x W을 의미
i : scale 크기, 그림에서 i의 최대값은 n
n : scale의 마지막
j : kernel size
k : stride</p>
</blockquote>
<h2 id="성능-지표">성능 지표</h2>
<p><img src="https://velog.velcdn.com/images/chan_woo_00/post/1c565ed2-3eea-4d54-814f-fbc3fe98f2bc/image.png" alt=""></p>
<p>GTX 2080Ti로 돌린 모델과 비교했을 때 MIoU값과 FPS가 높은 것을 확인할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Carla Simulator Actor 경로 설정]]></title>
            <link>https://velog.io/@chan_woo_00/Carla-Simulator-Server%EA%B2%BD%EB%A1%9C-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@chan_woo_00/Carla-Simulator-Server%EA%B2%BD%EB%A1%9C-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Sun, 24 Aug 2025 12:42:32 GMT</pubDate>
            <description><![CDATA[<h2 id="client에서-사용하는-알고리즘">Client에서 사용하는 알고리즘</h2>
<h3 id="carla-주행-경로-선택-과정">Carla 주행 경로 선택 과정</h3>
<p>Client의 ego 차량은 <code>PythonAPI.carla.agents.navigation</code>폴더의 모듈들에 의해서 경로를 설정하고 조정하는데 그 중 <code>global_route_planner</code>에 경로를 설정하는 알고리즘이 들어있다.</p>
<p><code>GlobalRoutePlanner</code>클래스를 생성할 때 <code>_build_topology()</code>함수를 통해 Carla Map의 Road Segment의 목록(list)을 받는다.
도로의 시작 waypoint와 끝 waypoint 객체에 대한 3D 좌표를 받고 도로에서 이동 가능한 영역의 waypoint들의 집합을 path에 저장한다.
path에 저장되는 값은 이동 가능한 waypoint들의 리스트를 나타내는 것으로 단위는 하나의 곡선 도로나 교차로와 교차로를 잇는 직선을 의미한다.(각 차선에 대해 구별-&gt; 2차선의 경우 2개)</p>
<p>path에 저장된 경로들은 <code>build_graph()</code>함수를 통해 실제 경로 탐색에 쓰일(<code>path_search</code>) netwrokx 그래프를 생성한다.</p>
<p>이후 <code>find_loose_ends()</code>와 <code>lane_change_link()</code>함수를 실행하여 그래프 상에 막다른 길이나 차선 변경이 가능한 영역에 대해 가중치 값이 0인 특수 엣지를 추가하는 등의 작업을 수행한다.
(가중치값이란 후에 A* 알고리즘을 통해 최단 경로 계산에 쓰이는 값을 의미)</p>
<p><code>GlobalRoutePlanner</code>클래스를 생성한 이후 <code>path_search()</code>함수를 반복적으로 실행하여 A* 알고리즘을 기반으로 경로 비용을 계산하여 최단 경로를 구성하는 path내에 포함된 값들의 리스트를 반환한다.</p>
<pre><code class="language-python">def _path_search(self, origin, destination):
    start, end = self._localize(origin), self._localize(destination)

    route = nx.astar_path(
        self._graph, source=start[0], target=end[0],
        heuristic=self._distance_heuristic, weight=&#39;length&#39;)
    route.append(end[1])
    return route</code></pre>
<h3 id="a-알고리즘-client"><a href="https://recall.tistory.com/40">A* 알고리즘</a> (Client)</h3>
<p>한점의 출발 지점에서 목표 지점까지 가는 최단 경로를 찾아내는 그래프 탐색 알고리즘으로 Carla에서 사용하는 path planning의 기반이다.</p>
<p>$$f(n) = g(n) + h(n)$$</p>
<ul>
<li>$$g(n)$$ : 출발 지점부터 현재 지점(n)까지에 대한 비용.</li>
<li>$$h(n)$$ : 현재 지점(n)부터 목표 지점까지 도달하기까지 예상되는 비용.</li>
<li>$$f(n)$$ : 총 예상 비용(출발 지점부터 목표 지점까지)</li>
</ul>
<p>n은 현재 위치로 현재 위치까지 오기까지의 비용과 앞으로 예상되는 비용들을 계산할 때 중심 위치이다.
n은 여러 위치에서 계산되며 그 중 최종 값인 f(n)이 가장 적게 나오는 값을 최단 경로로 설정한다.</p>
<pre><code>| 0 | 0 | 0 | 0 | 
| 0 | 0 | 0 | 0 | 
| 0 | 0 | 0 | 0 | 
| 0 | 0 | 0 | 0 | </code></pre><p>좌측 하단을 출발로 우측 상단으로 이동한다고 가정하고 한칸당 1의 비용을 가질 때
[1, 2]의 위치를 n이라고 가정하게 되면
n까지 가는 방법은 여러가지가 있게 된다.</p>
<pre><code>| 0 | 7 | 8 | 9 | 
| 0 | 6 | 0 | 0 | 
| 0 | 5 | 4 | 0 | 
| 1 | 2 | 3 | 0 | </code></pre><p>다음의 경로로 움직이게 될 경우 n을 가기까지 걸린 비용은 5가 되고 n부터 목표 지점까지는 4의 비용을 가져 총 9의 비용을 가진다.
$$f(n) = g(n) + h(n)$$ 가 $$9 = 5 + 4$$ 형태로 이루어진 것이다.</p>
<pre><code>| 6 | 7 | 8 | 9 | 
| 5 | 4 | 0 | 0 | 
| 0 | 3 | 0 | 0 | 
| 1 | 2 | 0 | 0 | </code></pre><p>다음의 경우는 $$f(n) = g(n) + h(n)$$ 가 $$9 = 3 + 6$$ 형태로 이루어진 것이다. </p>
<p>A* 알고리즘은 다음과 같은 그래프에서 최단 비용을 소모하는 경로를 찾아 최종 경로를 찾게 된다.</p>
<pre><code>| 0 | 0 | 0 | 7 | 
| 0 | 4 | 5 | 6 | 
| 0 | 3 | 0 | 0 | 
| 1 | 2 | 0 | 0 | </code></pre><p>$$f(n) = g(n) + h(n)$$ -&gt; $$7 = 3 + 4$$</p>
<p>차선 변경의 경우는 비용이 0이 되어 차선 변경이 들어가더라도 비용값에 대해 영향을 미치게 되지 않게 설정된다.</p>
<h2 id="server에서-사용하는-알고리즘">Server에서 사용하는 알고리즘</h2>
<p>Carla Simulator는 Server내에서 Actor의 상태를 파악하고 각 Actor가 현재 위치한 waypoint의 정보에 따라 차선, 신호등, 인근 차량 등을 고려하여 경로를 계산한다.
계산하는 주기는 <code>world.tick()</code>마다 반복된다.</p>
<p>경로 추종 제어의 순서는 다음과 같다.</p>
<h4 id="1-목표-지점waypoint설정">1. 목표 지점(waypoint)설정</h4>
<p>차량의 현재 위치에서 가능한 경로 중 다음 waypoint 하나를 목표 지점으로 선정.</p>
<h4 id="2-차량-제어-값-계산">2. 차량 제어 값 계산</h4>
<p>목표 waypoint까지 도달하기 위해 가속, 제동, 회전 등의 값을 계산.</p>
<h4 id="3-명령-수행">3. 명령 수행</h4>
<p>계산된 각 값들을 각 Actor에 전달하여 수행.</p>
<h4 id="4-최종-목적지까지-반복">4. 최종 목적지까지 반복</h4>
<p>각 <code>world.tick()</code>마다 1단계부터 3단계를 반복하여 수행.</p>
<p>A* 알고리즘을 사용하지 않는 이유는 Server에 돌아다니는 차량의 경우 최종 목적지가 정해지지 않고 waypoint(1m)단위로 랜덤으로 이동하기 때문이다.
따라서 매 <code>world.tick()</code>마다 이동 가능한 waypoint를 찾아 그 중 하나를 선택하고 이동하는 방식을 사용하기에 특정한 알고리즘을 사용하지 않는다.</p>
]]></description>
        </item>
    </channel>
</rss>